{ "openapi": "3.1.0", "info": { "title": "Scholia API", "description": "", "license": { "name": "" }, "version": "0.1.0" }, "paths": { "/api/admin/articles/{slug}/labels": { "post": { "tags": ["articles"], "summary": "Apply an editorial label to a published article. Editor/admin only.", "operationId": "apply_article_label", "parameters": [ { "name": "slug", "in": "path", "description": "Article slug", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApplyEditorialLabelRequest" } } }, "required": true }, "responses": { "200": { "description": "Label applied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EditorialLabelResponse" } } } }, "400": { "description": "Article not eligible (e.g. not published)" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Article or label not found" } } } }, "/api/admin/articles/{slug}/labels/{label_slug}": { "delete": { "tags": ["articles"], "summary": "Remove an editorial label from an article. Editor/admin only.\nIdempotent — returns 200 even if the label wasn't applied.", "operationId": "remove_article_label", "parameters": [ { "name": "slug", "in": "path", "description": "Article slug", "required": true, "schema": { "type": "string" } }, { "name": "label_slug", "in": "path", "description": "Label slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Label removed (or wasn't applied)" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } } }, "/api/admin/feedback": { "get": { "tags": ["feedback"], "summary": "List feedback (admin only). Default filter is \"active\" (todo + in_progress).", "operationId": "list_feedback", "parameters": [ { "name": "filter", "in": "query", "description": "Filter set: \"active\" (todo + in_progress, default), \"all\", or a\nspecific status name (\"todo\", \"in_progress\", \"done\", \"cancelled\").", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32", "minimum": 0 } }, { "name": "per_page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "Feedback list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FeedbackListResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "Not an admin" } } } }, "/api/admin/feedback/{id}": { "get": { "tags": ["feedback"], "summary": "Get a single feedback row (admin only).", "operationId": "get_feedback", "parameters": [ { "name": "id", "in": "path", "description": "Feedback ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Feedback detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FeedbackResponse" } } } }, "400": { "description": "Invalid feedback ID" }, "401": { "description": "Not authenticated" }, "404": { "description": "Not found / not an admin" } } }, "patch": { "tags": ["feedback"], "summary": "Update status and/or admin notes (admin only).", "operationId": "update_feedback", "parameters": [ { "name": "id", "in": "path", "description": "Feedback ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateFeedbackRequest" } } }, "required": true }, "responses": { "200": { "description": "Feedback updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FeedbackResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "404": { "description": "Not found / not an admin" } } } }, "/api/admin/resource-submissions": { "get": { "tags": ["resources"], "summary": "List community resource submissions for review (editors + admins).", "operationId": "list_resource_submissions", "parameters": [ { "name": "filter", "in": "query", "description": "Filter set: \"pending\" (default), \"all\", \"approved\", or \"rejected\".", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32", "minimum": 0 } }, { "name": "per_page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "Submission queue", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResourceSubmissionListResponse" } } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } } }, "/api/admin/resource-submissions/{id}": { "patch": { "tags": ["resources"], "summary": "Approve or reject a community submission (editors + admins).", "operationId": "review_resource_submission", "parameters": [ { "name": "id", "in": "path", "description": "Submission (resource) ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReviewSubmissionRequest" } } }, "required": true }, "responses": { "200": { "description": "Submission reviewed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResourceSubmissionResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Submission not found" } } } }, "/api/admin/users": { "get": { "tags": ["admin-users"], "summary": "List users with their roles, paginated and optionally searched.", "operationId": "list_users", "parameters": [ { "name": "search", "in": "query", "description": "Case-insensitive substring match on email, display name, or handle.", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32", "minimum": 0 } }, { "name": "per_page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "User list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminUserListResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "Insufficient permissions" } } } }, "/api/admin/users/{id}": { "get": { "tags": ["admin-users"], "summary": "Get a single user with their roles.", "operationId": "get_user", "parameters": [ { "name": "id", "in": "path", "description": "User ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "User detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminUserRow" } } } }, "400": { "description": "Invalid user ID" }, "401": { "description": "Not authenticated" }, "404": { "description": "Not found / insufficient permissions" } } } }, "/api/admin/users/{id}/roles": { "put": { "tags": ["admin-users"], "summary": "Replace a user's manageable roles. Rejects paid/unknown roles, refuses\nself-demotion out of admin, and (in the DB tx) refuses to remove the\nlast admin. On success, the target's sessions are invalidated so the\nchange takes effect on their next request.", "operationId": "set_user_roles", "parameters": [ { "name": "id", "in": "path", "description": "User ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetUserRolesRequest" } } }, "required": true }, "responses": { "200": { "description": "Roles updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminUserRow" } } } }, "400": { "description": "Invalid input / non-manageable role / last admin" }, "401": { "description": "Not authenticated" }, "404": { "description": "Not found / insufficient permissions" } } } }, "/api/article-quotations": { "get": { "tags": ["article-quotations"], "summary": "List current user's article quotations", "operationId": "list_article_quotations", "responses": { "200": { "description": "User's article quotations", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleQuotationListResponse" } } } }, "401": { "description": "Not authenticated" } } }, "post": { "tags": ["article-quotations"], "summary": "Save a quotation from an article (returns existing if duplicate)", "operationId": "create_article_quotation", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateArticleQuotationRequest" } } }, "required": true }, "responses": { "200": { "description": "Article quotation saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateArticleQuotationResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "404": { "description": "Article not found or not published" } } } }, "/api/article-quotations/{id}": { "get": { "tags": ["article-quotations"], "summary": "Get a single article quotation (for directive hydration)", "operationId": "get_article_quotation", "parameters": [ { "name": "id", "in": "path", "description": "Article quotation ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Article quotation detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleQuotationResponse" } } } }, "400": { "description": "Invalid quotation ID" }, "404": { "description": "Not found" } } }, "delete": { "tags": ["article-quotations"], "summary": "Delete an article quotation", "operationId": "delete_article_quotation", "parameters": [ { "name": "id", "in": "path", "description": "Article quotation ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Article quotation deleted" }, "400": { "description": "Invalid quotation ID" }, "401": { "description": "Not authenticated" }, "404": { "description": "Not found" } } } }, "/api/articles": { "get": { "tags": ["articles"], "summary": "List published articles", "operationId": "list_published_articles", "parameters": [ { "name": "topic_slug", "in": "query", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "label_slug", "in": "query", "description": "Filter the listing to articles bearing this editorial label slug\n(e.g. `featured`, `high-quality`). Single label per request — no\nAND-ing across labels in v1.", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32" } }, { "name": "per_page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32" } } ], "responses": { "200": { "description": "Published articles", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublishedArticleListResponse" } } } } } }, "post": { "tags": ["articles"], "summary": "Create a new article", "operationId": "create_article", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateArticleRequest" } } }, "required": true }, "responses": { "200": { "description": "Article created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleDetailResponse" } } } }, "400": { "description": "Invalid input or limit reached" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } } }, "/api/articles/by-id/{id}": { "get": { "tags": ["articles"], "summary": "Get a published/archived article by UUID (stable URL)", "operationId": "get_article_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Article UUID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Article detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleDetailResponse" } } } }, "400": { "description": "Invalid article ID" }, "404": { "description": "Article not found" } } } }, "/api/articles/{slug}": { "get": { "tags": ["articles"], "summary": "Get a published article by slug", "operationId": "get_published_article", "parameters": [ { "name": "slug", "in": "path", "description": "Article slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Published article", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleDetailResponse" } } } }, "404": { "description": "Article not found" } } } }, "/api/auth/forgot-password": { "post": { "tags": ["auth"], "summary": "Request a password reset email", "operationId": "forgot_password", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ForgotPasswordRequest" } } }, "required": true }, "responses": { "200": { "description": "If the email exists, a reset link was sent", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } } } } }, "/api/auth/github": { "get": { "tags": ["auth"], "summary": "Redirect to GitHub OAuth", "operationId": "github_login", "responses": { "302": { "description": "Redirect to GitHub" } } } }, "/api/auth/github/callback": { "get": { "tags": ["auth"], "summary": "Handle GitHub OAuth callback", "operationId": "github_callback", "parameters": [ { "name": "code", "in": "query", "description": "Authorization code", "required": true, "schema": { "type": "string" } }, { "name": "state", "in": "query", "description": "CSRF state", "required": true, "schema": { "type": "string" } } ], "responses": { "302": { "description": "Redirect to frontend" } } } }, "/api/auth/login": { "post": { "tags": ["auth"], "summary": "Log in with email and password", "operationId": "login", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginRequest" } } }, "required": true }, "responses": { "200": { "description": "Logged in", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Invalid credentials" }, "403": { "description": "Email not verified" } } } }, "/api/auth/logout": { "post": { "tags": ["auth"], "summary": "Log out (destroy session)", "operationId": "logout", "responses": { "200": { "description": "Logged out", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } } } } }, "/api/auth/me": { "get": { "tags": ["auth"], "summary": "Get current authenticated user", "operationId": "me", "responses": { "200": { "description": "Current user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthResponse" } } } }, "401": { "description": "Not authenticated" } } } }, "/api/auth/profile": { "get": { "tags": ["auth"], "summary": "Get current user's profile with linked providers", "operationId": "get_profile", "responses": { "200": { "description": "User profile", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProfileResponse" } } } }, "401": { "description": "Not authenticated" } } }, "put": { "tags": ["auth"], "summary": "Update display name", "operationId": "update_profile", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateProfileRequest" } } }, "required": true }, "responses": { "200": { "description": "Profile updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "409": { "description": "Handle already taken" } } } }, "/api/auth/register": { "post": { "tags": ["auth"], "summary": "Register a new user with email and password", "operationId": "register", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RegisterRequest" } } }, "required": true }, "responses": { "201": { "description": "User created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } }, "400": { "description": "Invalid input" }, "409": { "description": "Email already exists" } } } }, "/api/auth/request-password-change": { "post": { "tags": ["auth"], "summary": "Request a password change email (for authenticated users)", "operationId": "request_password_change", "responses": { "200": { "description": "Password change email sent", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } }, "400": { "description": "Rate limit reached" }, "401": { "description": "Not authenticated" } } } }, "/api/auth/reset-password": { "post": { "tags": ["auth"], "summary": "Reset password using a token", "operationId": "reset_password", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResetPasswordRequest" } } }, "required": true }, "responses": { "200": { "description": "Password reset", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } }, "400": { "description": "Invalid or expired token" } } } }, "/api/auth/verify-email": { "get": { "tags": ["auth"], "summary": "Verify email using a token", "operationId": "verify_email", "parameters": [ { "name": "token", "in": "query", "description": "Verification token", "required": true, "schema": { "type": "string" } } ], "responses": { "302": { "description": "Redirects to frontend" } } } }, "/api/billing/checkout": { "post": { "tags": ["billing"], "summary": "Create a Stripe Embedded Checkout Session for the given tier.\nLazily creates the user's Stripe customer on first call.", "operationId": "create_checkout_session", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCheckoutRequest" } } }, "required": true }, "responses": { "200": { "description": "Checkout session created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCheckoutResponse" } } } }, "400": { "description": "Invalid tier" }, "401": { "description": "Not authenticated" }, "409": { "description": "Already has an active subscription" }, "500": { "description": "Stripe API error" } } } }, "/api/billing/portal": { "post": { "tags": ["billing"], "summary": "Create a Stripe Customer Portal session and return the redirect URL.\nRequires the user to already have a `stripe_customer_id` (i.e. has\ninitiated at least one checkout). 404 otherwise.", "operationId": "create_portal_session", "responses": { "200": { "description": "Portal session created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PortalSessionResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "User has no Stripe customer yet" }, "500": { "description": "Stripe API error" } } } }, "/api/books": { "get": { "tags": ["books"], "summary": "List all books", "operationId": "list_books", "responses": { "200": { "description": "List of books", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/BookSummary" } } } } } } } }, "/api/books/{slug}": { "get": { "tags": ["books"], "summary": "Get a book by slug", "operationId": "get_book", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Book details", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookDetail" } } } }, "404": { "description": "Book not found" } } } }, "/api/books/{slug}/about": { "get": { "tags": ["books"], "summary": "Bibliographic info for the \"About this text\" panel. With `?node=`,\nwalks the toc-ancestor chain to surface the constituent work the\nreader is in (e.g. Genesis within KJV); otherwise returns the\nhosted book's source.", "operationId": "get_book_about", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "node", "in": "query", "description": "Active toc node slug for contextual resolution", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Bibliographic info for the active text", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AboutThisTextResponse" } } } }, "404": { "description": "Book or node not found" } } } }, "/api/books/{slug}/article-references": { "get": { "tags": ["articles"], "summary": "List published articles quoting a sentence range (public)", "operationId": "list_article_references", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "start", "in": "query", "required": true, "schema": { "type": "integer", "format": "int32" } }, { "name": "end", "in": "query", "required": true, "schema": { "type": "integer", "format": "int32" } }, { "name": "kind", "in": "query", "required": false, "schema": { "type": "string" } }, { "name": "limit", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int64" } }, { "name": "offset", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int64" } } ], "responses": { "200": { "description": "Articles quoting the passage, across translations of the same work", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PassageArticleListResponse" } } } }, "404": { "description": "Book not found" } } } }, "/api/books/{slug}/nodes": { "get": { "tags": ["nodes"], "summary": "Get paginated nodes for infinite scroll", "operationId": "get_node_page", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "after", "in": "query", "description": "sort_order cursor — fetch nodes after this value", "required": false, "schema": { "type": ["integer", "null"], "format": "int32" } }, { "name": "before", "in": "query", "description": "sort_order cursor — fetch nodes before this value", "required": false, "schema": { "type": ["integer", "null"], "format": "int32" } }, { "name": "limit", "in": "query", "description": "page size, default 20, max 50", "required": false, "schema": { "type": ["integer", "null"], "format": "int32" } }, { "name": "original", "in": "query", "description": "include original_text/original_html fields", "required": false, "schema": { "type": ["boolean", "null"] } }, { "name": "source_nodes", "in": "query", "description": "Comma-separated source node UUIDs — fetch nodes whose source_node_id matches", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "node_ids", "in": "query", "description": "Comma-separated node UUIDs — fetch nodes by their own ID", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "at", "in": "query", "description": "Anchor node slug — return a forward window centered on this node\n(combine with `back` to include nodes before the anchor)", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "back", "in": "query", "description": "Number of nodes to include before the `at` anchor (default 0)", "required": false, "schema": { "type": ["integer", "null"], "format": "int32" } } ], "responses": { "200": { "description": "Page of nodes with content", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NodePage" } } } }, "400": { "description": "Too many node_ids/source_nodes" }, "404": { "description": "Book not found" } } } }, "/api/books/{slug}/nodes/{node_slug}": { "get": { "tags": ["nodes"], "summary": "Get node content (blocks + sentences)", "operationId": "get_node", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "node_slug", "in": "path", "description": "Node slug", "required": true, "schema": { "type": "string" } }, { "name": "original", "in": "query", "description": "include original_text/original_html fields", "required": false, "schema": { "type": ["boolean", "null"] } } ], "responses": { "200": { "description": "Node with content blocks and sentences", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NodeDetail" } } } }, "404": { "description": "Node not found" } } } }, "/api/books/{slug}/nodes/{node_slug}/meta": { "get": { "tags": ["nodes"], "summary": "Get node metadata (SEO excerpt)", "operationId": "get_node_meta", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "node_slug", "in": "path", "description": "Node slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Node metadata", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NodeMetaResponse" } } } }, "404": { "description": "Node not found" } } } }, "/api/books/{slug}/quotations": { "get": { "tags": ["quotations"], "summary": "List quotations for a node (authenticated user only)", "operationId": "list_quotations", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "node_id", "in": "query", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "User's quotations for the node", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/QuotationListResponse" } } } }, "400": { "description": "Invalid node ID" }, "401": { "description": "Not authenticated" }, "404": { "description": "Book not found" } } }, "post": { "tags": ["quotations"], "summary": "Save a quotation (returns existing if duplicate)", "operationId": "create_quotation", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateQuotationRequest" } } }, "required": true }, "responses": { "200": { "description": "Quotation saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateQuotationResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "404": { "description": "Book not found" } } } }, "/api/books/{slug}/quotations/{id}": { "delete": { "tags": ["quotations"], "summary": "Delete a quotation (cascade-deletes notes)", "operationId": "delete_quotation", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "id", "in": "path", "description": "Quotation ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Quotation deleted" }, "400": { "description": "Invalid quotation ID" }, "401": { "description": "Not authenticated" }, "404": { "description": "Quotation not found" } } } }, "/api/books/{slug}/quotations/{id}/notes": { "get": { "tags": ["quotations"], "summary": "List notes for a quotation", "operationId": "list_notes", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "id", "in": "path", "description": "Quotation ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Notes for the quotation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NoteListResponse" } } } }, "400": { "description": "Invalid quotation ID" }, "401": { "description": "Not authenticated" }, "404": { "description": "Quotation not found" } } }, "post": { "tags": ["quotations"], "summary": "Create a note on a quotation", "operationId": "create_note", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "id", "in": "path", "description": "Quotation ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateNoteRequest" } } }, "required": true }, "responses": { "200": { "description": "Note created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NoteResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "404": { "description": "Quotation not found" } } } }, "/api/books/{slug}/quotations/{id}/notes/{note_id}": { "put": { "tags": ["quotations"], "summary": "Update a note", "operationId": "update_note", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "id", "in": "path", "description": "Quotation ID", "required": true, "schema": { "type": "string" } }, { "name": "note_id", "in": "path", "description": "Note ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateNoteRequest" } } }, "required": true }, "responses": { "200": { "description": "Note updated" }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "403": { "description": "Not the note owner" }, "404": { "description": "Note not found" } } }, "delete": { "tags": ["quotations"], "summary": "Delete a note", "operationId": "delete_note", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "id", "in": "path", "description": "Quotation ID", "required": true, "schema": { "type": "string" } }, { "name": "note_id", "in": "path", "description": "Note ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Note deleted" }, "400": { "description": "Invalid note ID" }, "401": { "description": "Not authenticated" }, "404": { "description": "Note not found" } } } }, "/api/books/{slug}/resources": { "get": { "tags": ["resources"], "summary": "List resources for a sentence range (public)", "operationId": "list_resources", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "start", "in": "query", "required": true, "schema": { "type": "integer", "format": "int32" } }, { "name": "end", "in": "query", "required": true, "schema": { "type": "integer", "format": "int32" } }, { "name": "kind", "in": "query", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Resources for the sentence range", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResourceListResponse" } } } }, "404": { "description": "Book not found" } } }, "post": { "tags": ["resources"], "summary": "Create a resource. Any authenticated user may submit; editors\n(`ResourcesManage`) publish immediately, everyone else's suggestion enters\nthe review queue as `pending` until an editor approves it.", "operationId": "create_resource", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateResourceRequest" } } }, "required": true }, "responses": { "200": { "description": "Resource created or submitted for review", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResourceListResponse" } } } }, "400": { "description": "Invalid input or rate limit reached" }, "401": { "description": "Not authenticated" }, "404": { "description": "Book not found" } } } }, "/api/books/{slug}/resources/{id}": { "put": { "tags": ["resources"], "summary": "Update an existing resource (editor only)", "operationId": "update_resource", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "id", "in": "path", "description": "Resource ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateResourceRequest" } } }, "required": true }, "responses": { "200": { "description": "Resource updated" }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Resource not found" } } }, "delete": { "tags": ["resources"], "summary": "Soft-delete a resource (editor only)", "operationId": "delete_resource", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } }, { "name": "id", "in": "path", "description": "Resource ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Resource archived" }, "400": { "description": "Invalid resource ID" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Resource not found" } } } }, "/api/books/{slug}/toc": { "get": { "tags": ["toc"], "summary": "Get the full TOC tree for a book", "operationId": "get_toc", "parameters": [ { "name": "slug", "in": "path", "description": "Book slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "TOC tree", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/TocNodeResponse" } } } } }, "404": { "description": "Book not found" } } } }, "/api/editorial-labels": { "get": { "tags": ["articles"], "summary": "List all editorial labels. Public — readers see chip metadata so the\nfrontend can render names/slugs without a separate lookup, and editors\nuse the same endpoint to populate the manage-labels modal.", "operationId": "list_editorial_labels", "responses": { "200": { "description": "Editorial labels", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EditorialLabelListResponse" } } } } } } }, "/api/feedback": { "post": { "tags": ["feedback"], "summary": "Submit feedback to admins. Auth-required; rate-limited per user.", "operationId": "create_feedback", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateFeedbackRequest" } } }, "required": true }, "responses": { "200": { "description": "Feedback submitted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FeedbackResponse" } } } }, "400": { "description": "Invalid input or rate limit reached" }, "401": { "description": "Not authenticated" } } } }, "/api/library": { "get": { "tags": ["books"], "summary": "Get the grouped library tree (authors → works → versions) with stats.", "operationId": "get_library", "responses": { "200": { "description": "Library tree", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LibraryResponse" } } } } } } }, "/api/notes": { "get": { "tags": ["quotations"], "summary": "List all notes for the authenticated user (across all books)", "operationId": "list_all_notes", "parameters": [ { "name": "book_slug", "in": "query", "required": false, "schema": { "type": ["string", "null"] } } ], "responses": { "200": { "description": "User's notes across books", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NoteWithContextListResponse" } } } }, "401": { "description": "Not authenticated" } } } }, "/api/persons": { "get": { "tags": ["persons"], "summary": "Search persons by name", "operationId": "search_persons", "parameters": [ { "name": "q", "in": "query", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "List of matching persons", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/PersonResponse" } } } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } }, "post": { "tags": ["persons"], "summary": "Create a new person", "operationId": "create_person", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePersonRequest" } } }, "required": true }, "responses": { "200": { "description": "Person created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PersonResponse" } } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } } }, "/api/persons/{id}": { "put": { "tags": ["persons"], "summary": "Update an existing person", "operationId": "update_person", "parameters": [ { "name": "id", "in": "path", "description": "Person ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdatePersonRequest" } } }, "required": true }, "responses": { "200": { "description": "Person updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PersonResponse" } } } }, "400": { "description": "Invalid person ID" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Person not found" } } } }, "/api/quotations": { "get": { "tags": ["quotations"], "summary": "List all quotations for the authenticated user (books + articles, unified)", "operationId": "list_all_quotations", "parameters": [ { "name": "book_slug", "in": "query", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "source_type", "in": "query", "required": false, "schema": { "type": ["string", "null"] } } ], "responses": { "200": { "description": "User's quotations across books and articles", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnifiedQuotationListResponse" } } } }, "401": { "description": "Not authenticated" } } } }, "/api/sentences/batch": { "post": { "tags": ["sentences"], "summary": "Batch fetch sentences for quotation card hydration", "operationId": "batch_sentences", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BatchSentencesRequest" } } }, "required": true }, "responses": { "200": { "description": "Sentence data", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BatchSentencesResponse" } } } }, "404": { "description": "Book or node not found" } } } }, "/api/sources": { "get": { "tags": ["sources"], "summary": "Search sources by title", "operationId": "search_sources", "parameters": [ { "name": "q", "in": "query", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "List of matching sources", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/SourceSearchResponse" } } } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } }, "post": { "tags": ["sources"], "summary": "Create a new source", "operationId": "create_source", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSourceRequest" } } }, "required": true }, "responses": { "200": { "description": "Source created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SourceResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } } }, "/api/sources/browse": { "get": { "tags": ["sources"], "summary": "Browse sources (paginated, with filters)", "operationId": "browse_sources", "parameters": [ { "name": "q", "in": "query", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "source_type", "in": "query", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "created_by_me", "in": "query", "required": false, "schema": { "type": ["boolean", "null"] } }, { "name": "protected", "in": "query", "required": false, "schema": { "type": ["boolean", "null"] } }, { "name": "page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32" } }, { "name": "per_page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32" } } ], "responses": { "200": { "description": "Paginated sources", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SourceBrowseResponse" } } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } } }, "/api/sources/{id}": { "get": { "tags": ["sources"], "summary": "Get a source by ID", "operationId": "get_source", "parameters": [ { "name": "id", "in": "path", "description": "Source ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Source details", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SourceResponse" } } } }, "400": { "description": "Invalid source ID" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Source not found" } } }, "put": { "tags": ["sources"], "summary": "Update an existing source", "operationId": "update_source", "parameters": [ { "name": "id", "in": "path", "description": "Source ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateSourceRequest" } } }, "required": true }, "responses": { "200": { "description": "Source updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SourceResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Source not found" } } }, "delete": { "tags": ["sources"], "summary": "Delete a source. Blocks when any references exist (resources, child\nsources, or article citations). Creator-only for non-editors.", "operationId": "delete_source", "parameters": [ { "name": "id", "in": "path", "description": "Source ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Source deleted" }, "400": { "description": "Source has active references" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Source not found" } } } }, "/api/sources/{id}/persons": { "post": { "tags": ["sources"], "summary": "Link a person to a source with a role", "operationId": "add_source_person", "parameters": [ { "name": "id", "in": "path", "description": "Source ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LinkSourcePersonRequest" } } }, "required": true }, "responses": { "200": { "description": "Person linked to source" }, "400": { "description": "Invalid ID" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Source not found" } } } }, "/api/sources/{id}/persons/{person_id}/{role}": { "delete": { "tags": ["sources"], "summary": "Remove a person from a source", "operationId": "remove_source_person", "parameters": [ { "name": "id", "in": "path", "description": "Source ID", "required": true, "schema": { "type": "string" } }, { "name": "person_id", "in": "path", "description": "Person ID", "required": true, "schema": { "type": "string" } }, { "name": "role", "in": "path", "description": "Role to remove", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Person unlinked from source" }, "400": { "description": "Invalid ID" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Source not found" } } } }, "/api/sources/{id}/references": { "get": { "tags": ["sources"], "summary": "Check if a source can be deleted (has active references)", "operationId": "check_source_references", "parameters": [ { "name": "id", "in": "path", "description": "Source ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Reference check result", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReferenceCheckResponse" } } } }, "400": { "description": "Invalid source ID" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } } }, "/api/tags": { "get": { "tags": ["tags"], "summary": "List all tags for the authenticated user", "operationId": "list_tags", "responses": { "200": { "description": "User's tags", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TagListResponse" } } } }, "401": { "description": "Not authenticated" } } } }, "/api/topics": { "get": { "tags": ["topics"], "summary": "List all topics", "operationId": "list_topics", "responses": { "200": { "description": "All topics", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TopicListResponse" } } } } } } }, "/api/user/articles": { "get": { "tags": ["articles"], "summary": "List current user's articles", "operationId": "list_user_articles", "parameters": [ { "name": "status", "in": "query", "required": false, "schema": { "type": ["string", "null"] } } ], "responses": { "200": { "description": "User's articles", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleListResponse" } } } }, "401": { "description": "Not authenticated" } } } }, "/api/user/articles/{slug}": { "get": { "tags": ["articles"], "summary": "Get a specific article for editing (owner only)", "operationId": "get_user_article", "parameters": [ { "name": "slug", "in": "path", "description": "Article slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Article detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleDetailResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "Article not found" } } }, "put": { "tags": ["articles"], "summary": "Update an article (owner only)", "operationId": "update_article", "parameters": [ { "name": "slug", "in": "path", "description": "Article slug", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArticleRequest" } } }, "required": true }, "responses": { "200": { "description": "Article updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleDetailResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "403": { "description": "Article edit limit reached" }, "404": { "description": "Article not found" } } } }, "/api/user/articles/{slug}/archive": { "post": { "tags": ["articles"], "summary": "Archive an article (one-way from published)", "operationId": "archive_article", "parameters": [ { "name": "slug", "in": "path", "description": "Article slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Article archived" }, "401": { "description": "Not authenticated" }, "404": { "description": "Article not found" } } } }, "/api/user/articles/{slug}/publish": { "post": { "tags": ["articles"], "summary": "Publish an article", "operationId": "publish_article", "parameters": [ { "name": "slug", "in": "path", "description": "Article slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Article published" }, "400": { "description": "Publish limit reached" }, "401": { "description": "Not authenticated" }, "403": { "description": "Article edit limit reached" }, "404": { "description": "Article not found" } } } }, "/api/users/by-id/{id}": { "get": { "tags": ["users"], "summary": "Resolve a user UUID to the user's *current* handle. Frontend uses\nthis to redirect from durable URLs (`/users/by-id/`) to the\ncanonical handle URL.", "operationId": "get_handle_by_id", "parameters": [ { "name": "id", "in": "path", "description": "User UUID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Current handle", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserHandleResponse" } } } }, "400": { "description": "Invalid UUID" }, "404": { "description": "User not found" } } } }, "/api/users/{handle}": { "get": { "tags": ["users"], "summary": "Public profile by handle.", "operationId": "get_public_profile", "parameters": [ { "name": "handle", "in": "path", "description": "User handle", "required": true, "schema": { "type": "string" } }, { "name": "topic_slug", "in": "query", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "label_slug", "in": "query", "description": "Filter the listing to articles bearing this editorial label slug\n(e.g. `featured`, `high-quality`). Single label per request — no\nAND-ing across labels in v1.", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32" } }, { "name": "per_page", "in": "query", "required": false, "schema": { "type": ["integer", "null"], "format": "int32" } } ], "responses": { "200": { "description": "User profile", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicProfileResponse" } } } }, "400": { "description": "Invalid handle" }, "404": { "description": "User not found" } } } } }, "components": { "schemas": { "AboutThisTextResponse": { "type": "object", "description": "Resolved bibliographic info for the \"About this text\" panel.\n\nWalks ancestors of the active toc node looking for the deepest\n`toc_nodes.source_id` and falls back to `books.source_id`. For\nBible-shape works this lets the panel show info about the\nconstituent book the reader is in (Genesis, Romans); for plain\nworks (Kant) it always falls back to the hosted book's source.", "required": ["source", "resolved_from"], "properties": { "about_text": { "type": ["string", "null"], "description": "Editor's note for this hosted edition (free-form plain text).\nMirrors `books.about_text` of the hosted book the reader is in\n(NOT walked across toc ancestors)." }, "resolved_from": { "type": "string", "description": "`\"node\"` when an ancestor toc_node supplied a source_id (Bible\nbooks-within-books); `\"book\"` when the resolver fell back to\nthe hosted book's root source." }, "source": { "$ref": "#/components/schemas/SourceResponse" }, "source_book": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/BookSummary", "description": "Hosted source-language book, when this source is a translation\nand the original is also hosted (Kant EN → Kant DE). Absent for\nBible-shape works whose originals aren't hosted." } ] } } }, "AdminUserListResponse": { "type": "object", "required": ["users", "total", "page", "per_page"], "properties": { "page": { "type": "integer", "format": "int32", "minimum": 0 }, "per_page": { "type": "integer", "format": "int32", "minimum": 0 }, "total": { "type": "integer", "format": "int64" }, "users": { "type": "array", "items": { "$ref": "#/components/schemas/AdminUserRow" } } } }, "AdminUserRow": { "type": "object", "required": [ "id", "email", "display_name", "roles", "email_verified", "created_at" ], "properties": { "created_at": { "type": "string" }, "display_name": { "type": "string" }, "email": { "type": "string" }, "email_verified": { "type": "boolean" }, "handle": { "type": ["string", "null"] }, "id": { "type": "string" }, "roles": { "type": "array", "items": { "type": "string" }, "description": "All roles the user holds, including the default `user` role and any\npaid (Stripe-managed) tiers. The UI renders paid roles read-only." } } }, "ApplyEditorialLabelRequest": { "type": "object", "required": ["label_slug"], "properties": { "label_slug": { "type": "string" } } }, "ArticleDetailResponse": { "type": "object", "required": [ "id", "title", "slug", "markdown", "html", "status", "author_user_id", "author_display_name", "author_public_roles", "topics", "labels", "created_at", "updated_at" ], "properties": { "author_display_name": { "type": "string" }, "author_handle": { "type": ["string", "null"] }, "author_public_roles": { "type": "array", "items": { "type": "string" } }, "author_user_id": { "type": "string" }, "created_at": { "type": "string" }, "description": { "type": ["string", "null"] }, "html": { "type": "string" }, "id": { "type": "string" }, "labels": { "type": "array", "items": { "$ref": "#/components/schemas/EditorialLabelResponse" } }, "markdown": { "type": "string" }, "published_at": { "type": ["string", "null"] }, "revoked_labels": { "type": "array", "items": { "$ref": "#/components/schemas/EditorialLabelResponse" }, "description": "Labels whose `revokes_on_edit` flag is `true` and which were\nstripped by the most recent author edit. Empty unless this\nresponse is the result of a markdown update that revoked chips.\nFrontend uses this to toast the author." }, "slug": { "type": "string" }, "status": { "type": "string" }, "title": { "type": "string" }, "topics": { "type": "array", "items": { "$ref": "#/components/schemas/TopicResponse" } }, "updated_at": { "type": "string" } } }, "ArticleLimitsResponse": { "type": "object", "required": [ "max_active", "current_active", "max_archive", "current_archive" ], "properties": { "current_active": { "type": "integer", "format": "int64" }, "current_archive": { "type": "integer", "format": "int64" }, "max_active": { "type": "integer", "format": "int32" }, "max_archive": { "type": "integer", "format": "int32" } } }, "ArticleListResponse": { "type": "object", "required": ["articles", "limits"], "properties": { "articles": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleResponse" } }, "limits": { "$ref": "#/components/schemas/ArticleLimitsResponse" } } }, "ArticleQuotationListResponse": { "type": "object", "required": ["article_quotations"], "properties": { "article_quotations": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleQuotationResponse" } } } }, "ArticleQuotationResponse": { "type": "object", "required": [ "id", "article_title", "author_display_name", "text", "html", "note_count", "created_at" ], "properties": { "article_id": { "type": ["string", "null"] }, "article_title": { "type": "string" }, "author_display_name": { "type": "string" }, "created_at": { "type": "string" }, "html": { "type": "string" }, "id": { "type": "string" }, "note_count": { "type": "integer", "format": "int64" }, "text": { "type": "string" } } }, "ArticleResponse": { "type": "object", "required": [ "id", "title", "slug", "status", "author_user_id", "author_display_name", "author_public_roles", "topics", "labels", "created_at", "updated_at" ], "properties": { "author_display_name": { "type": "string" }, "author_handle": { "type": ["string", "null"], "description": "Current handle of the author. Use `/users/` for the\ncanonical link, or `/users/by-id/` for a\nrename-durable link." }, "author_public_roles": { "type": "array", "items": { "type": "string" }, "description": "Public-facing role chips for the author (`editor`, paid tiers)." }, "author_user_id": { "type": "string" }, "created_at": { "type": "string" }, "description": { "type": ["string", "null"] }, "id": { "type": "string" }, "labels": { "type": "array", "items": { "$ref": "#/components/schemas/EditorialLabelResponse" }, "description": "Editor/admin-applied editorial labels. Empty for drafts and for\narticles no editor has chipped. Ordered by `editorial_labels.sort_order`." }, "published_at": { "type": ["string", "null"] }, "slug": { "type": "string" }, "status": { "type": "string" }, "title": { "type": "string" }, "topics": { "type": "array", "items": { "$ref": "#/components/schemas/TopicResponse" } }, "updated_at": { "type": "string" } } }, "AuthResponse": { "type": "object", "required": [ "id", "email", "display_name", "roles", "permissions" ], "properties": { "avatar_url": { "type": ["string", "null"] }, "display_name": { "type": "string" }, "email": { "type": "string" }, "id": { "type": "string" }, "permissions": { "type": "array", "items": { "type": "string" } }, "roles": { "type": "array", "items": { "type": "string" } } } }, "BatchSentenceRequest": { "type": "object", "required": ["book_slug", "node_slug", "start_number", "kind"], "properties": { "book_slug": { "type": "string" }, "end_number": { "type": ["integer", "null"], "format": "int32" }, "kind": { "type": "string" }, "node_slug": { "type": "string" }, "start_number": { "type": "integer", "format": "int32" } } }, "BatchSentenceResponseItem": { "type": "object", "required": [ "book_slug", "book_title", "node_slug", "node_label", "citation", "sentences" ], "properties": { "book_slug": { "type": "string" }, "book_title": { "type": "string" }, "citation": { "type": "array", "items": { "$ref": "#/components/schemas/CitationPart" }, "description": "Resolved citation parts (ordered by `cite_priority`). Empty = no\ndefault citation system; the card falls back to `s. N`." }, "node_label": { "type": "string" }, "node_slug": { "type": "string" }, "parent_node_label": { "type": ["string", "null"], "description": "Label of the cited node's parent in the toc tree, when one\nexists. For bibles this is the bible-book (\"Romans\"); for Milton\nthe work (\"Paradise Lost\"). Substituted into `{parent}` of a\ncitation template." }, "sentences": { "type": "array", "items": { "$ref": "#/components/schemas/SentenceData" } }, "source": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SourceContext" } ] } } }, "BatchSentencesRequest": { "type": "object", "required": ["items"], "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/BatchSentenceRequest" } } } }, "BatchSentencesResponse": { "type": "object", "required": ["items"], "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/BatchSentenceResponseItem" } } } }, "BookDetail": { "type": "object", "required": [ "id", "slug", "title", "author", "language", "source_id", "translations", "sibling_translations" ], "properties": { "author": { "type": "string" }, "id": { "type": "string" }, "language": { "type": "string" }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "publisher": { "type": ["string", "null"] }, "sibling_translations": { "type": "array", "items": { "$ref": "#/components/schemas/BookSummary" }, "description": "Sibling translations: every other book that shares this book's\ntranslation root. Populated when the work has no hosted source\nlanguage (Bible: KJV book has [WEB]) so the reader's flat\ntranslation picker can list peer translations regardless of\nsource/translation polarity. Empty for Kant-style works where\n`source_book_slug` and `translations` already express the\nrelationship." }, "slug": { "type": "string" }, "source_book_slug": { "type": ["string", "null"], "description": "The hosted source-language book this book translates from\n(Kant EN → Kant DE). Absent when there is no hosted source —\ne.g. Bible translations from Hebrew/Greek that we don't host." }, "source_id": { "type": "string" }, "title": { "type": "string" }, "translations": { "type": "array", "items": { "$ref": "#/components/schemas/BookSummary" }, "description": "Books translated from THIS book. Populated for source-language\nbooks (Kant DE → [Kant EN]); empty for translation leaves." } } }, "BookPill": { "type": "object", "description": "One entry in a Bible-shape group's primary pill row.\n`node_slug` is the toc-node slug (translation-invariant by import\nguard) — the frontend composes the URL as\n`/books//`.", "required": ["node_slug", "label", "sort_order"], "properties": { "label": { "type": "string" }, "node_slug": { "type": "string" }, "sort_order": { "type": "integer", "format": "int32" } } }, "BookSummary": { "type": "object", "required": ["id", "slug", "title", "author", "language"], "properties": { "author": { "type": "string" }, "id": { "type": "string" }, "language": { "type": "string" }, "slug": { "type": "string" }, "title": { "type": "string" } } }, "CitationPart": { "type": "object", "description": "One system's contribution to a quotation's citation, resolved over the cited\nrange. `template` carries `{parent}`/`{self}`/`{ref}` tokens (the frontend\nsubstitutes the node labels and the `first_ref`[–`last_ref`] range). Parts\nare ordered by the system's `cite_priority`; multiple parts are joined for\nmulti-system citations (e.g. Kant A/B). An empty list means the book has no\ndefault citation system, so the card falls back to `s. N`.", "required": ["template", "first_ref"], "properties": { "first_ref": { "type": "string" }, "last_ref": { "type": ["string", "null"] }, "template": { "type": "string" } } }, "ContentBlockResponse": { "type": "object", "required": [ "id", "position", "block_type", "html", "sentences" ], "properties": { "block_type": { "type": "string" }, "figure_number": { "type": ["integer", "null"], "format": "int32" }, "html": { "type": "string" }, "id": { "type": "string" }, "original_html": { "type": ["string", "null"] }, "paragraph_number": { "type": ["integer", "null"], "format": "int32" }, "position": { "type": "integer", "format": "int32" }, "sentences": { "type": "array", "items": { "$ref": "#/components/schemas/SentenceResponse" } } } }, "CreateArticleQuotationRequest": { "type": "object", "required": ["article_id", "text", "html"], "properties": { "article_id": { "type": "string" }, "html": { "type": "string" }, "text": { "type": "string" } } }, "CreateArticleQuotationResponse": { "type": "object", "required": ["article_quotation", "created"], "properties": { "article_quotation": { "$ref": "#/components/schemas/ArticleQuotationResponse" }, "created": { "type": "boolean" } } }, "CreateArticleRequest": { "type": "object", "required": ["title"], "properties": { "title": { "type": "string" } } }, "CreateCheckoutRequest": { "type": "object", "required": ["tier"], "properties": { "tier": { "type": "string", "description": "Tier slug: \"base\" | \"mid\" | \"high\"." } } }, "CreateCheckoutResponse": { "type": "object", "required": ["client_secret"], "properties": { "client_secret": { "type": "string", "description": "Pass to the Stripe Embedded Checkout React component." } } }, "CreateFeedbackRequest": { "type": "object", "required": ["body"], "properties": { "body": { "type": "string" }, "url": { "type": ["string", "null"], "description": "Optional. Full URL (path + query) the user was on when submitting." }, "user_agent": { "type": ["string", "null"] }, "viewport_h": { "type": ["integer", "null"], "format": "int32" }, "viewport_w": { "type": ["integer", "null"], "format": "int32" } } }, "CreateNoteRequest": { "type": "object", "required": ["body"], "properties": { "body": { "type": "string" }, "tags": { "type": "array", "items": { "type": "string" } } } }, "CreatePersonRequest": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" }, "sort_name": { "type": ["string", "null"] } } }, "CreateQuotationRequest": { "type": "object", "required": ["sentence_start", "sentence_kind"], "properties": { "sentence_end": { "type": ["integer", "null"], "format": "int32" }, "sentence_kind": { "type": "string" }, "sentence_start": { "type": "integer", "format": "int32" } } }, "CreateQuotationResponse": { "type": "object", "required": ["quotation", "created"], "properties": { "created": { "type": "boolean" }, "quotation": { "$ref": "#/components/schemas/QuotationResponse" } } }, "CreateResourceRequest": { "type": "object", "required": [ "resource_type", "sentence_start", "sentence_kind" ], "properties": { "admin_notes": { "type": ["string", "null"] }, "editor_note": { "type": ["string", "null"] }, "is_featured": { "type": ["boolean", "null"] }, "quoted_text": { "type": ["string", "null"] }, "resource_type": { "type": "string" }, "scope": { "type": ["string", "null"], "description": "\"work\" (default) | \"language\" | \"edition\" — see ResourceResponse.scope." }, "sentence_end": { "type": ["integer", "null"], "format": "int32" }, "sentence_kind": { "type": "string" }, "sentence_start": { "type": "integer", "format": "int32" }, "source_id": { "type": ["string", "null"] }, "source_location_freeform": { "type": ["string", "null"] }, "source_page_end": { "type": ["integer", "null"], "format": "int32" }, "source_page_start": { "type": ["integer", "null"], "format": "int32" }, "verbatim_kind": { "type": ["string", "null"] } } }, "CreateSourceRequest": { "type": "object", "required": ["source_type", "title"], "properties": { "doi": { "type": ["string", "null"] }, "edition": { "type": ["string", "null"] }, "isbn": { "type": ["array", "null"], "items": { "type": "string" } }, "journal_name": { "type": ["string", "null"] }, "page_end": { "type": ["integer", "null"], "format": "int32" }, "page_start": { "type": ["integer", "null"], "format": "int32" }, "parent_source_id": { "type": ["string", "null"] }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "publisher": { "type": ["string", "null"] }, "source_type": { "type": "string" }, "title": { "type": "string" }, "title_display": { "type": ["string", "null"] }, "translation_of_id": { "type": ["string", "null"] }, "url": { "type": ["string", "null"] }, "volume": { "type": ["string", "null"] } } }, "EditorialLabelListResponse": { "type": "object", "required": ["labels"], "properties": { "labels": { "type": "array", "items": { "$ref": "#/components/schemas/EditorialLabelResponse" } } } }, "EditorialLabelResponse": { "type": "object", "description": "Public-facing editorial label. `applied_by`/`applied_at` are\ndeliberately not exposed — readers don't need to know which editor\nchipped an article or when.\n\n`revokes_on_edit` IS exposed so the article editor can warn authors\nbefore they make changes that would strip the chip; it's a property\nof the label itself, not user-specific or sensitive.", "required": ["id", "name", "slug", "revokes_on_edit"], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "revokes_on_edit": { "type": "boolean" }, "slug": { "type": "string" } } }, "FeedbackHandler": { "type": "object", "required": ["id", "display_name"], "properties": { "display_name": { "type": "string" }, "id": { "type": "string" } } }, "FeedbackListResponse": { "type": "object", "required": ["feedback", "total", "page", "per_page"], "properties": { "feedback": { "type": "array", "items": { "$ref": "#/components/schemas/FeedbackResponse" } }, "page": { "type": "integer", "format": "int32", "minimum": 0 }, "per_page": { "type": "integer", "format": "int32", "minimum": 0 }, "total": { "type": "integer", "format": "int64" } } }, "FeedbackResponse": { "type": "object", "required": [ "id", "body", "status", "created_at", "updated_at" ], "properties": { "admin_notes": { "type": ["string", "null"] }, "body": { "type": "string" }, "created_at": { "type": "string" }, "handled_by": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/FeedbackHandler" } ] }, "id": { "type": "string" }, "status": { "$ref": "#/components/schemas/FeedbackStatus" }, "submitter": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/FeedbackSubmitter", "description": "`None` when the submitter's account has been deleted; the admin UI\nrenders \"User deleted\" in that case." } ] }, "updated_at": { "type": "string" }, "url": { "type": ["string", "null"] }, "user_agent": { "type": ["string", "null"] }, "viewport_h": { "type": ["integer", "null"], "format": "int32" }, "viewport_w": { "type": ["integer", "null"], "format": "int32" } } }, "FeedbackStatus": { "type": "string", "enum": ["todo", "in_progress", "done", "cancelled"] }, "FeedbackSubmitter": { "type": "object", "required": ["id", "display_name", "email"], "properties": { "display_name": { "type": "string" }, "email": { "type": "string" }, "id": { "type": "string" } } }, "FootnoteResponse": { "type": "object", "required": ["id", "number", "sentences"], "properties": { "id": { "type": "string" }, "number": { "type": "integer", "format": "int32" }, "sentences": { "type": "array", "items": { "$ref": "#/components/schemas/FootnoteSentenceResponse" } } } }, "FootnoteSentenceResponse": { "type": "object", "required": ["id", "position", "text", "html"], "properties": { "html": { "type": "string" }, "id": { "type": "string" }, "original_html": { "type": ["string", "null"] }, "original_text": { "type": ["string", "null"] }, "position": { "type": "integer", "format": "int32" }, "sentence_number": { "type": ["integer", "null"], "format": "int32" }, "text": { "type": "string" } } }, "ForgotPasswordRequest": { "type": "object", "required": ["email"], "properties": { "email": { "type": "string" } } }, "LibraryGroup": { "type": "object", "description": "A library entry as the index renders it: a heading plus a list of\nworks under it. The heading is either an author (`primary_kind =\n\"author\"`) or the work itself (`primary_kind = \"self\"` — used for\nauthorless top-level works, including compilations like the Bible\nand singletons like Gilgamesh).", "required": [ "id", "primary_kind", "primary_label", "sort_name", "books", "book_pills" ], "properties": { "book_pills": { "type": "array", "items": { "$ref": "#/components/schemas/BookPill" }, "description": "\"Bible-shape\" navigation: when this group is a compilation that is\navailable in multiple translations, the frontend renders the\ncompilation's child works (e.g. Genesis, John) as primary book\npills here, and the multiple translations collapse into a single\nsubtle translation chooser. Empty for regular author groups and\nfor compilations available in a single translation." }, "books": { "type": "array", "items": { "$ref": "#/components/schemas/LibraryWork" } }, "id": { "type": "string", "description": "Stable identifier. Person id for `\"author\"` groups; source id\nfor `\"self\"` groups. UUIDs are globally unique so the id is\nsafe to use as a React key without a kind prefix." }, "primary_kind": { "type": "string", "description": "`\"author\"` — heading is a person; `books` are works by them.\n`\"self\"` — heading IS the work; `books` are its child sources\n(compilation case) or empty (singleton case). Click the\nheading to open the work in the reader." }, "primary_label": { "type": "string" }, "primary_slug": { "type": ["string", "null"], "description": "Reader path for `primary_kind = \"self\"` headings (e.g.\n`/books/king-james-bible`). Absent for author headings until\nper-author pages exist." }, "sort_name": { "type": "string" } } }, "LibraryResponse": { "type": "object", "required": ["groups", "stats"], "properties": { "groups": { "type": "array", "items": { "$ref": "#/components/schemas/LibraryGroup" } }, "stats": { "$ref": "#/components/schemas/LibraryStats" } } }, "LibraryStats": { "type": "object", "required": ["works", "authors", "languages"], "properties": { "authors": { "type": "integer", "format": "int64" }, "languages": { "type": "integer", "format": "int64" }, "works": { "type": "integer", "format": "int64" } } }, "LibraryVersion": { "type": "object", "required": [ "book_slug", "language", "is_original", "translator_names" ], "properties": { "book_slug": { "type": "string" }, "is_original": { "type": "boolean" }, "language": { "type": "string" }, "node_slug": { "type": ["string", "null"], "description": "For nested works (a child source hosted inside a compilation):\nthe toc-node slug to deep-link to. Absent for top-level books." }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "publisher": { "type": ["string", "null"] }, "translator_names": { "type": "array", "items": { "type": "string" } } } }, "LibraryWork": { "type": "object", "required": ["work_id", "title", "co_authors", "versions"], "properties": { "co_authors": { "type": "array", "items": { "type": "string" } }, "editor_names": { "type": ["array", "null"], "items": { "type": "string" } }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "title": { "type": "string" }, "versions": { "type": "array", "items": { "$ref": "#/components/schemas/LibraryVersion" } }, "work_id": { "type": "string" } } }, "LinkSourcePersonRequest": { "type": "object", "required": ["person_id", "role"], "properties": { "person_id": { "type": "string" }, "position": { "type": ["integer", "null"], "format": "int32" }, "role": { "type": "string" } } }, "LinkedProvider": { "type": "object", "required": ["provider"], "properties": { "email": { "type": ["string", "null"] }, "provider": { "type": "string" } } }, "LoginRequest": { "type": "object", "required": ["email", "password"], "properties": { "email": { "type": "string" }, "password": { "type": "string" } } }, "MessageResponse": { "type": "object", "required": ["message"], "properties": { "message": { "type": "string" } } }, "NodeDetail": { "type": "object", "required": [ "id", "source_ref", "slug", "label", "depth", "sort_order", "blocks" ], "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/ContentBlockResponse" } }, "depth": { "type": "integer", "format": "int32" }, "id": { "type": "string" }, "label": { "type": "string" }, "slug": { "type": "string" }, "sort_order": { "type": "integer", "format": "int32" }, "source_node_id": { "type": ["string", "null"] }, "source_ref": { "type": "string" } } }, "NodeMetaResponse": { "type": "object", "description": "Lightweight per-node metadata for SEO: the opening text of the\nnode's content, for meta descriptions and social previews. `null`\nfor structural nodes without content blocks.", "properties": { "excerpt": { "type": ["string", "null"] } } }, "NodePage": { "type": "object", "required": ["nodes", "has_more", "has_previous"], "properties": { "has_more": { "type": "boolean" }, "has_previous": { "type": "boolean" }, "nodes": { "type": "array", "items": { "$ref": "#/components/schemas/NodeDetail" } } } }, "NoteLimitsResponse": { "type": "object", "required": ["max", "current"], "properties": { "current": { "type": "integer", "format": "int64" }, "max": { "type": "integer", "format": "int32" } } }, "NoteListResponse": { "type": "object", "required": ["notes"], "properties": { "notes": { "type": "array", "items": { "$ref": "#/components/schemas/NoteResponse" } } } }, "NoteResponse": { "type": "object", "required": ["id", "body", "tags", "created_at", "updated_at"], "properties": { "body": { "type": "string" }, "created_at": { "type": "string" }, "id": { "type": "string" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/TagResponse" } }, "updated_at": { "type": "string" } } }, "NoteWithContextListResponse": { "type": "object", "required": ["notes", "limits"], "properties": { "limits": { "$ref": "#/components/schemas/NoteLimitsResponse" }, "notes": { "type": "array", "items": { "$ref": "#/components/schemas/NoteWithContextResponse" } } } }, "NoteWithContextResponse": { "type": "object", "required": [ "id", "body", "tags", "book_slug", "book_title", "node_label", "node_slug", "anchor_sentence_start_number", "sentence_kind", "quotation_id", "created_at", "updated_at" ], "properties": { "anchor_main_sentence_number": { "type": ["integer", "null"], "format": "int32", "description": "For footnote-kind anchors: the body sentence number the footnote is\nattached to. None for body-kind anchors." }, "anchor_sentence_end_number": { "type": ["integer", "null"], "format": "int32" }, "anchor_sentence_start_number": { "type": "integer", "format": "int32" }, "body": { "type": "string" }, "book_slug": { "type": "string" }, "book_title": { "type": "string", "description": "See `QuotationWithContextResponse::book_title`." }, "created_at": { "type": "string" }, "id": { "type": "string" }, "node_label": { "type": "string" }, "node_slug": { "type": "string" }, "parent_compilation_title": { "type": ["string", "null"], "description": "See `QuotationWithContextResponse::parent_compilation_title`." }, "quotation_id": { "type": "string" }, "sentence_kind": { "type": "string" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/TagResponse" } }, "translation_label": { "type": ["string", "null"], "description": "See `QuotationWithContextResponse::translation_label`." }, "updated_at": { "type": "string" } } }, "PageMarkerResponse": { "type": "object", "required": ["system_slug", "ref_value", "sort_order"], "properties": { "char_offset": { "type": ["integer", "null"], "format": "int32" }, "image_url": { "type": ["string", "null"] }, "ref_value": { "type": "string" }, "sort_order": { "type": "integer", "format": "int32" }, "system_slug": { "type": "string" } } }, "ParentSourceResponse": { "type": "object", "required": ["id", "source_type", "title", "persons"], "properties": { "id": { "type": "string" }, "persons": { "type": "array", "items": { "$ref": "#/components/schemas/SourcePersonResponse" } }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "publisher": { "type": ["string", "null"] }, "source_type": { "type": "string" }, "title": { "type": "string" } } }, "PassageArticleListResponse": { "type": "object", "required": ["articles", "total"], "properties": { "articles": { "type": "array", "items": { "$ref": "#/components/schemas/PassageArticleResponse" } }, "total": { "type": "integer", "format": "int64" } } }, "PassageArticleOrigin": { "type": "object", "description": "One edition an article's matched references anchor on.", "required": ["book_slug", "language"], "properties": { "book_slug": { "type": "string" }, "language": { "type": "string" } } }, "PassageArticleResponse": { "type": "object", "required": [ "id", "slug", "title", "author_user_id", "author_display_name", "origins" ], "properties": { "author_display_name": { "type": "string" }, "author_handle": { "type": ["string", "null"], "description": "Current handle of the author. Use `/users/` for the\ncanonical link, or `/users/by-id/` for a\nrename-durable link." }, "author_user_id": { "type": "string" }, "id": { "type": "string" }, "origins": { "type": "array", "items": { "$ref": "#/components/schemas/PassageArticleOrigin" }, "description": "The editions whose passages this article's matched references\nanchor on, for the selection being viewed. Contains the requested\nbook itself when the article quotes it directly; other entries\nmean the article reached this selection across editions (ADR\n0008) — the reader badges those by language/edition." }, "published_at": { "type": ["string", "null"] }, "slug": { "type": "string" }, "title": { "type": "string" } } }, "PersonResponse": { "type": "object", "required": ["id", "name", "created_by", "protected"], "properties": { "created_by": { "type": "string" }, "id": { "type": "string" }, "name": { "type": "string" }, "protected": { "type": "boolean" }, "sort_name": { "type": ["string", "null"] } } }, "PortalSessionResponse": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string", "description": "Stripe-hosted Customer Portal URL; redirect the browser here." } } }, "ProfileResponse": { "type": "object", "required": [ "id", "email", "display_name", "has_password", "roles", "providers" ], "properties": { "avatar_url": { "type": ["string", "null"] }, "bio": { "type": ["string", "null"] }, "display_name": { "type": "string" }, "email": { "type": "string" }, "handle": { "type": ["string", "null"], "description": "Public URL identifier (`/users/`)." }, "handle_changed_at": { "type": ["string", "null"], "description": "RFC3339 timestamp of the most recent handle change. Used by the\nfrontend to compute remaining cooldown days." }, "has_password": { "type": "boolean" }, "id": { "type": "string" }, "location": { "type": ["string", "null"] }, "providers": { "type": "array", "items": { "$ref": "#/components/schemas/LinkedProvider" } }, "roles": { "type": "array", "items": { "type": "string" } }, "sort_name": { "type": ["string", "null"], "description": "Bibliography sort key, \"Last, First\" form. Auto-derived at signup." }, "title": { "type": ["string", "null"] }, "website_url": { "type": ["string", "null"] } } }, "PublicProfileResponse": { "type": "object", "description": "Public-facing user profile, returned by `GET /api/users/{handle}`.\nDistinct from `ProfileResponse` (the self-edit shape under\n`GET /api/auth/profile`) because it strips email and operational fields.", "required": [ "id", "handle", "display_name", "public_roles", "created_at", "articles", "article_total" ], "properties": { "article_total": { "type": "integer", "format": "int64" }, "articles": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleResponse" }, "description": "First page of the user's published articles, newest first." }, "avatar_url": { "type": ["string", "null"] }, "bio": { "type": ["string", "null"] }, "created_at": { "type": "string", "description": "Member-since timestamp (RFC3339)." }, "display_name": { "type": "string" }, "handle": { "type": "string" }, "id": { "type": "string" }, "location": { "type": ["string", "null"] }, "public_roles": { "type": "array", "items": { "type": "string" }, "description": "Roles to render as chips: filtered server-side to public-facing\nroles only (excludes `admin` and the default `user`)." }, "title": { "type": ["string", "null"] }, "website_url": { "type": ["string", "null"] } } }, "PublishedArticleListResponse": { "type": "object", "required": ["articles", "total"], "properties": { "articles": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleResponse" } }, "total": { "type": "integer", "format": "int64" } } }, "QuotationLimitsResponse": { "type": "object", "required": ["max", "current"], "properties": { "current": { "type": "integer", "format": "int64" }, "max": { "type": "integer", "format": "int32" } } }, "QuotationListResponse": { "type": "object", "required": ["quotations"], "properties": { "quotations": { "type": "array", "items": { "$ref": "#/components/schemas/QuotationResponse" } } } }, "QuotationResponse": { "type": "object", "required": [ "id", "anchor_sentence_start_number", "sentence_kind", "note_count", "created_at" ], "properties": { "anchor_sentence_end_number": { "type": ["integer", "null"], "format": "int32" }, "anchor_sentence_start_number": { "type": "integer", "format": "int32" }, "anchor_source_ref": { "type": ["string", "null"], "description": "`toc_nodes.source_ref` of the anchor — translation-invariant\n(e.g. `\"genesis:5\"`). Used as the chapter-grouping key for\nverse-level visual marker projection." }, "anchor_verse_end": { "type": ["string", "null"], "description": "Verse `ref_value` of the end anchor sentence (only when the\nquotation spans multiple verses; otherwise null and the\nquotation is treated as covering a single verse)." }, "anchor_verse_start": { "type": ["string", "null"], "description": "Verse `ref_value` of the start anchor sentence (e.g. `\"5:1\"`).\nPopulated when the book has a `verse` reference system; null\nfor books without verse-style markers (Kant). Together with\n`anchor_source_ref` this is the cross-translation marker key." }, "book_slug": { "type": ["string", "null"], "description": "Slug of the book this quotation lives in. Used to render the\ntranslation badge (\"KJV\"/\"WEB\" — see `translation_label`) and\nto differentiate cross-translation peer quotations from\nown-book ones in the reader. Optional for backward-compat\nacross endpoints that haven't been wired yet; populated by\n`list_quotations_for_node`." }, "created_at": { "type": "string" }, "id": { "type": "string" }, "note_count": { "type": "integer", "format": "int64" }, "projected_source_ref": { "type": ["string", "null"], "description": "Target-local source_ref where the projection marker should\nrender. For same-book quotes equals `anchor_source_ref`. For\ncross-translation projection through `cross_translation_alignments`\nthis resolves to the target chapter (e.g. WEB Rom 14:24 quote\nprojects to KJV Rom 16:25 → projected_source_ref = \"romans:16\")." }, "projected_verse_end": { "type": ["string", "null"], "description": "Target-local verse `ref_value` for the end of the projection.\nEquals `projected_verse_start` when only one end of a range\nprojects into the target chapter (e.g. cross-chapter spans)." }, "projected_verse_start": { "type": ["string", "null"], "description": "Target-local verse `ref_value` for the start of the projection.\nFrontend uses `(projected_source_ref, projected_verse_start)`\nas the marker placement key, falling back to `anchor_*` when\nthe projection fields are absent (Kant/no-verse-system books)." }, "sentence_kind": { "type": "string" }, "translation_label": { "type": ["string", "null"], "description": "Short display label for the translation badge — derived from\nthe source's `publisher` field (which the Bible importer sets\nto \"KJV\"/\"WEB\"). Falls back to the book title when absent." } } }, "QuotationWithContextListResponse": { "type": "object", "required": ["quotations"], "properties": { "quotations": { "type": "array", "items": { "$ref": "#/components/schemas/QuotationWithContextResponse" } } } }, "QuotationWithContextResponse": { "type": "object", "required": [ "id", "book_slug", "book_title", "node_label", "node_slug", "anchor_sentence_start_number", "sentence_kind", "has_source_view", "note_count", "created_at" ], "properties": { "anchor_main_sentence_number": { "type": ["integer", "null"], "format": "int32", "description": "For footnote-kind anchors: the body sentence number the footnote is\nattached to. None for body-kind anchors." }, "anchor_sentence_end_number": { "type": ["integer", "null"], "format": "int32" }, "anchor_sentence_start_number": { "type": "integer", "format": "int32" }, "book_slug": { "type": "string" }, "book_title": { "type": "string", "description": "Title of the cited bibliographic work (Shape 3): the effective\nsource resolved for the quotation's anchor — either the per-book\nchild source within a compilation (e.g. \"Genesis\") or the hosted\ntext's root source for non-compilations (e.g. \"Critique of Pure\nReason\")." }, "created_at": { "type": "string" }, "end_text_snippet": { "type": ["string", "null"] }, "has_source_view": { "type": "boolean", "description": "`true` when the cited book is a translation AND the original work\nis also a hosted text (i.e. there's a `books` row for the\n`translation_of_id` source). Used by the quotation embed picker\nto decide whether the \"Source\" / \"Both\" display modes are\nmeaningful — for translations whose original isn't hosted (e.g.\nBible translations under the canonical \"The Bible\" root), only\n\"Translation\" mode makes sense." }, "id": { "type": "string" }, "node_label": { "type": "string" }, "node_slug": { "type": "string" }, "note_count": { "type": "integer", "format": "int64" }, "parent_compilation_title": { "type": ["string", "null"], "description": "When the cited source is a child of a compilation, the compilation's\ndisplay title. Frontend renders as \"[book_title] (in: [parent])\"." }, "sentence_kind": { "type": "string" }, "start_text_snippet": { "type": ["string", "null"] }, "translation_label": { "type": ["string", "null"], "description": "Compact translation badge — the source's `publisher` when short\n(e.g. \"KJV\"/\"WEB\" for Bible) or the language code (e.g. \"DE\"/\"EN\"\nfor Kant). Used in My Quotations / reader badge UI to show\nwhich translation a quotation belongs to." } } }, "ReferenceCheckResponse": { "type": "object", "required": ["total", "resources", "child_sources", "articles"], "properties": { "articles": { "$ref": "#/components/schemas/ReferencedArticles" }, "child_sources": { "$ref": "#/components/schemas/ReferencedChildSources" }, "resources": { "$ref": "#/components/schemas/ReferencedResources" }, "total": { "type": "integer", "format": "int64", "description": "Total count across all categories." } } }, "ReferencedArticle": { "type": "object", "required": ["id", "title", "slug", "status", "is_mine"], "properties": { "id": { "type": "string" }, "is_mine": { "type": "boolean" }, "slug": { "type": "string" }, "status": { "type": "string" }, "title": { "type": "string" } } }, "ReferencedArticles": { "type": "object", "required": ["count", "items"], "properties": { "count": { "type": "integer", "format": "int64" }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/ReferencedArticle" } } } }, "ReferencedChildSource": { "type": "object", "required": ["id", "title", "relation"], "properties": { "id": { "type": "string" }, "relation": { "type": "string" }, "title": { "type": "string" } } }, "ReferencedChildSources": { "type": "object", "required": ["count", "items"], "properties": { "count": { "type": "integer", "format": "int64" }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/ReferencedChildSource" } } } }, "ReferencedResources": { "type": "object", "required": ["count", "ids"], "properties": { "count": { "type": "integer", "format": "int64" }, "ids": { "type": "array", "items": { "type": "string" } } } }, "RegisterRequest": { "type": "object", "required": ["email", "display_name", "password"], "properties": { "display_name": { "type": "string" }, "email": { "type": "string" }, "password": { "type": "string" } } }, "ResetPasswordRequest": { "type": "object", "required": ["token", "password"], "properties": { "password": { "type": "string" }, "token": { "type": "string" } } }, "ResourceListResponse": { "type": "object", "required": ["resources"], "properties": { "resources": { "type": "array", "items": { "$ref": "#/components/schemas/ResourceResponse" } } } }, "ResourceResponse": { "type": "object", "required": [ "id", "resource_type", "anchor_sentence_start_number", "sentence_kind", "is_featured", "review_status", "scope", "is_projected", "created_at" ], "properties": { "admin_notes": { "type": ["string", "null"] }, "anchor_sentence_end_number": { "type": ["integer", "null"], "format": "int32" }, "anchor_sentence_start_number": { "type": "integer", "format": "int32" }, "created_at": { "type": "string" }, "editor_note": { "type": ["string", "null"] }, "id": { "type": "string" }, "is_featured": { "type": "boolean" }, "is_projected": { "type": "boolean", "description": "True when this entry is projected from a sibling edition of the same\nwork (its anchor lives in `origin_book_slug`, not the requested book).\nProjected entries are read-only in the requesting edition." }, "origin_book_slug": { "type": ["string", "null"] }, "origin_language": { "type": ["string", "null"] }, "projected_sentence_end_number": { "type": ["integer", "null"], "format": "int32" }, "projected_sentence_start_number": { "type": ["integer", "null"], "format": "int32", "description": "Target-local sentence range covering the projected anchor, for\nplacement in the requesting edition. Absent when the anchor's\npassage has no counterpart there." }, "quoted_text": { "type": ["string", "null"] }, "resource_type": { "type": "string" }, "review_status": { "type": "string", "description": "Lifecycle: \"approved\" for live resources, \"pending\" for a community\nsubmission awaiting review, \"rejected\" for a declined one. The reader\nonly ever receives \"approved\" rows plus the caller's own \"pending\" ones." }, "scope": { "type": "string", "description": "Cataloguer's claim about what the resource is about: \"work\" (the\npassage in any form), \"language\" (this language's translation layer),\nor \"edition\" (this one edition's actual text). Label only — never\naffects which editions the resource appears on." }, "sentence_kind": { "type": "string" }, "source": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SourceResponse" } ] }, "source_location_freeform": { "type": ["string", "null"] }, "source_page_end": { "type": ["integer", "null"], "format": "int32" }, "source_page_start": { "type": ["integer", "null"], "format": "int32" }, "verbatim_kind": { "type": ["string", "null"] } } }, "ResourceReviewStatus": { "type": "string", "description": "Review state of a resource that entered via community submission.", "enum": ["pending", "approved", "rejected"] }, "ResourceSubmissionListResponse": { "type": "object", "required": ["submissions", "total", "page", "per_page"], "properties": { "page": { "type": "integer", "format": "int32", "minimum": 0 }, "per_page": { "type": "integer", "format": "int32", "minimum": 0 }, "submissions": { "type": "array", "items": { "$ref": "#/components/schemas/ResourceSubmissionResponse" } }, "total": { "type": "integer", "format": "int64" } } }, "ResourceSubmissionResponse": { "type": "object", "description": "One entry in the editor review queue: the proposed resource plus the\ncontext an editor needs to judge it (where it anchors, who suggested it).", "required": ["resource", "book_slug", "book_title"], "properties": { "book_slug": { "type": "string" }, "book_title": { "type": "string" }, "resource": { "$ref": "#/components/schemas/ResourceResponse", "description": "The proposed resource, in the same shape the reader renders. `review_status`\non this object carries the submission's current state." }, "review_note": { "type": ["string", "null"] }, "reviewed_at": { "type": ["string", "null"] }, "submitter": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ResourceSubmitter", "description": "`None` when the submitter's account has since been deleted." } ] } } }, "ResourceSubmitter": { "type": "object", "required": ["id", "display_name"], "properties": { "display_name": { "type": "string" }, "id": { "type": "string" } } }, "ReviewSubmissionRequest": { "type": "object", "description": "An editor's verdict on a submission. `status` must be `approved` or\n`rejected`; `review_note` is an optional message (e.g. a rejection reason).", "required": ["status"], "properties": { "review_note": { "type": ["string", "null"] }, "status": { "$ref": "#/components/schemas/ResourceReviewStatus" } } }, "SentenceData": { "type": "object", "required": ["sentence_number", "html"], "properties": { "html": { "type": "string" }, "original_html": { "type": ["string", "null"] }, "sentence_number": { "type": "integer", "format": "int32" } } }, "SentenceResponse": { "type": "object", "required": ["id", "position", "text", "html", "page_markers"], "properties": { "figure_number": { "type": ["integer", "null"], "format": "int32", "description": "Set only on a figure's anchor sentence — drives the `fig{N}` selection key." }, "footnotes": { "type": "array", "items": { "$ref": "#/components/schemas/FootnoteResponse" } }, "html": { "type": "string" }, "id": { "type": "string" }, "original_html": { "type": ["string", "null"] }, "original_text": { "type": ["string", "null"] }, "page_markers": { "type": "array", "items": { "$ref": "#/components/schemas/PageMarkerResponse" } }, "position": { "type": "integer", "format": "int32" }, "segment": { "type": ["integer", "null"], "format": "int32", "description": "1-based index of the indented run within the paragraph (`+ ` enumerations),\nor absent for normal flow. Consecutive same-`segment` sentences render as\none hanging-indent block." }, "sentence_number": { "type": ["integer", "null"], "format": "int32" }, "source_sentence_end_id": { "type": ["string", "null"] }, "source_sentence_start_id": { "type": ["string", "null"] }, "text": { "type": "string" } } }, "SetUserRolesRequest": { "type": "object", "required": ["roles"], "properties": { "roles": { "type": "array", "items": { "type": "string" }, "description": "The full desired set of *manageable* roles for the user (see\n`MANAGEABLE_ROLES`). Paid tiers and the default `user` role are\nleft untouched; passing a non-manageable role is rejected." } } }, "SourceBrowseResponse": { "type": "object", "required": ["sources", "total"], "properties": { "sources": { "type": "array", "items": { "$ref": "#/components/schemas/SourceSearchResponse" } }, "total": { "type": "integer", "format": "int64" } } }, "SourceContext": { "type": "object", "required": [ "book_slug", "book_title", "node_slug", "node_label" ], "properties": { "book_slug": { "type": "string" }, "book_title": { "type": "string" }, "node_label": { "type": "string" }, "node_slug": { "type": "string" } } }, "SourcePersonResponse": { "type": "object", "required": [ "person_id", "name", "role", "position", "created_by", "protected" ], "properties": { "created_by": { "type": "string" }, "name": { "type": "string" }, "person_id": { "type": "string" }, "position": { "type": "integer", "format": "int32" }, "protected": { "type": "boolean" }, "role": { "type": "string" }, "sort_name": { "type": ["string", "null"] } } }, "SourceResponse": { "type": "object", "required": [ "id", "source_type", "title", "created_by", "protected", "persons" ], "properties": { "created_by": { "type": "string" }, "doi": { "type": ["string", "null"] }, "edition": { "type": ["string", "null"] }, "id": { "type": "string" }, "isbn": { "type": ["array", "null"], "items": { "type": "string" } }, "journal_name": { "type": ["string", "null"] }, "page_end": { "type": ["integer", "null"], "format": "int32" }, "page_start": { "type": ["integer", "null"], "format": "int32" }, "parent": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ParentSourceResponse" } ] }, "persons": { "type": "array", "items": { "$ref": "#/components/schemas/SourcePersonResponse" } }, "protected": { "type": "boolean" }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "publisher": { "type": ["string", "null"] }, "source_type": { "type": "string" }, "title": { "type": "string" }, "title_display": { "type": ["string", "null"] }, "translation_of_id": { "type": ["string", "null"] }, "url": { "type": ["string", "null"] }, "volume": { "type": ["string", "null"] } } }, "SourceSearchResponse": { "type": "object", "required": [ "id", "source_type", "title", "created_by", "protected", "persons" ], "properties": { "created_by": { "type": "string" }, "id": { "type": "string" }, "persons": { "type": "array", "items": { "$ref": "#/components/schemas/SourcePersonResponse" } }, "protected": { "type": "boolean" }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "source_type": { "type": "string" }, "title": { "type": "string" } } }, "TagListResponse": { "type": "object", "required": ["tags"], "properties": { "tags": { "type": "array", "items": { "$ref": "#/components/schemas/TagResponse" } } } }, "TagResponse": { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "string" }, "name": { "type": "string" } } }, "TocNodeResponse": { "type": "object", "required": [ "id", "source_ref", "slug", "label", "depth", "sort_order", "has_content", "children" ], "properties": { "children": { "type": "array", "items": { "$ref": "#/components/schemas/TocNodeResponse" } }, "depth": { "type": "integer" }, "has_content": { "type": "boolean" }, "id": { "type": "string" }, "label": { "type": "string" }, "label_html": { "type": "string" }, "slug": { "type": "string" }, "sort_order": { "type": "integer" }, "source_id": { "type": "string" }, "source_node_id": { "type": "string" }, "source_ref": { "type": "string" } } }, "TopicListResponse": { "type": "object", "required": ["topics"], "properties": { "topics": { "type": "array", "items": { "$ref": "#/components/schemas/TopicResponse" } } } }, "TopicResponse": { "type": "object", "required": ["id", "name", "slug"], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "slug": { "type": "string" } } }, "UnifiedQuotationListResponse": { "type": "object", "required": ["quotations", "limits"], "properties": { "limits": { "$ref": "#/components/schemas/QuotationLimitsResponse" }, "quotations": { "type": "array", "items": { "$ref": "#/components/schemas/UnifiedQuotationResponse" } } } }, "UnifiedQuotationResponse": { "oneOf": [ { "type": "object", "required": [ "id", "book_slug", "book_title", "node_label", "node_slug", "anchor_sentence_start_number", "sentence_kind", "has_source_view", "note_count", "created_at", "source_type" ], "properties": { "anchor_main_sentence_number": { "type": ["integer", "null"], "format": "int32", "description": "For footnote-kind anchors: the body sentence number the footnote\nis attached to. None for body-kind anchors." }, "anchor_sentence_end_number": { "type": ["integer", "null"], "format": "int32" }, "anchor_sentence_start_number": { "type": "integer", "format": "int32" }, "book_slug": { "type": "string" }, "book_title": { "type": "string" }, "created_at": { "type": "string" }, "end_text_snippet": { "type": ["string", "null"] }, "has_source_view": { "type": "boolean", "description": "See `QuotationWithContextResponse::has_source_view`." }, "id": { "type": "string" }, "node_label": { "type": "string" }, "node_slug": { "type": "string" }, "note_count": { "type": "integer", "format": "int64" }, "sentence_kind": { "type": "string" }, "source_type": { "type": "string", "enum": ["book"] }, "start_text_snippet": { "type": ["string", "null"] }, "translation_label": { "type": ["string", "null"], "description": "See `QuotationWithContextResponse::translation_label`." } } }, { "type": "object", "required": [ "id", "article_title", "author_display_name", "text_snippet", "note_count", "created_at", "source_type" ], "properties": { "article_id": { "type": ["string", "null"] }, "article_title": { "type": "string" }, "author_display_name": { "type": "string" }, "created_at": { "type": "string" }, "id": { "type": "string" }, "note_count": { "type": "integer", "format": "int64" }, "source_type": { "type": "string", "enum": ["article"] }, "text_snippet": { "type": "string" } } } ] }, "UpdateArticleRequest": { "type": "object", "properties": { "description": { "type": ["string", "null"] }, "markdown": { "type": ["string", "null"] }, "title": { "type": ["string", "null"] }, "topic_ids": { "type": ["array", "null"], "items": { "type": "string" } } } }, "UpdateFeedbackRequest": { "type": "object", "properties": { "admin_notes": { "type": ["string", "null"] }, "status": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/FeedbackStatus", "description": "Omit either field to leave it unchanged." } ] } } }, "UpdateNoteRequest": { "type": "object", "properties": { "body": { "type": ["string", "null"] }, "tags": { "type": ["array", "null"], "items": { "type": "string" } } } }, "UpdatePersonRequest": { "type": "object", "properties": { "name": { "type": ["string", "null"] }, "sort_name": { "type": ["string", "null"] } } }, "UpdateProfileRequest": { "type": "object", "required": ["display_name"], "properties": { "bio": { "type": ["string", "null"], "description": "Profile-page fields. Empty string clears." }, "display_name": { "type": "string" }, "handle": { "type": ["string", "null"], "description": "Optional. If present and different from current, attempts a rename\n— subject to charset, reservation, and 30-day cooldown rules." }, "location": { "type": ["string", "null"] }, "sort_name": { "type": ["string", "null"], "description": "Optional. Whitespace-only or empty resets to the auto-derived form." }, "title": { "type": ["string", "null"] }, "website_url": { "type": ["string", "null"] } } }, "UpdateResourceRequest": { "type": "object", "properties": { "admin_notes": { "type": ["string", "null"] }, "editor_note": { "type": ["string", "null"] }, "is_featured": { "type": ["boolean", "null"] }, "quoted_text": { "type": ["string", "null"] }, "resource_type": { "type": ["string", "null"] }, "scope": { "type": ["string", "null"], "description": "NOT NULL column: plain Option — omit to leave unchanged." }, "sentence_end": { "type": ["integer", "null"], "format": "int32" }, "sentence_kind": { "type": ["string", "null"] }, "sentence_start": { "type": ["integer", "null"], "format": "int32" }, "source_id": { "type": ["string", "null"] }, "source_location_freeform": { "type": ["string", "null"] }, "source_page_end": { "type": ["integer", "null"], "format": "int32" }, "source_page_start": { "type": ["integer", "null"], "format": "int32" }, "verbatim_kind": { "type": ["string", "null"] } } }, "UpdateSourceRequest": { "type": "object", "description": "Patch for a source. Nullable columns use `Option>` (via\n`double_option`) so an omitted field is left unchanged while an explicit\n`null` clears the column. `title` (NOT NULL) and `source_type` (immutable)\nstay plain `Option`.", "properties": { "doi": { "type": ["string", "null"] }, "edition": { "type": ["string", "null"] }, "isbn": { "type": ["array", "null"], "items": { "type": "string" } }, "journal_name": { "type": ["string", "null"] }, "page_end": { "type": ["integer", "null"], "format": "int32" }, "page_start": { "type": ["integer", "null"], "format": "int32" }, "parent_source_id": { "type": ["string", "null"] }, "protected": { "type": ["boolean", "null"] }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "publisher": { "type": ["string", "null"] }, "source_type": { "type": ["string", "null"] }, "title": { "type": ["string", "null"] }, "title_display": { "type": ["string", "null"] }, "translation_of_id": { "type": ["string", "null"] }, "url": { "type": ["string", "null"] }, "volume": { "type": ["string", "null"] } } }, "UserHandleResponse": { "type": "object", "required": ["handle"], "properties": { "handle": { "type": "string" } } } } } }