{ "openapi": "3.1.0", "info": { "title": "Scholia API", "description": "", "license": { "name": "" }, "version": "0.1.0" }, "paths": { "/api/admin/article-review-requests": { "get": { "tags": ["article-reviews"], "summary": "Editor queue of review requests.", "operationId": "list_article_review_queue", "parameters": [ { "name": "filter", "in": "query", "description": "`pending` (default) | `approved` | `declined` | `resolved` | `all`.", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "assignee", "in": "query", "description": "Filter by assignee: a reviewer's user id, or `unassigned`.\nAbsent = everyone.", "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": "Review queue", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewQueueResponse" } } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } } }, "/api/admin/article-review-requests/{id}": { "patch": { "tags": ["article-reviews"], "summary": "Decide a pending request. Approving a publication-intent request\npublishes the article (if still a draft) and applies the Imprimatur\nlabel; feedback-intent requests close as `resolved`.", "operationId": "decide_article_review", "parameters": [ { "name": "id", "in": "path", "description": "Review request ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReviewDecisionRequest" } } }, "required": true }, "responses": { "200": { "description": "Request decided", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewRequestResponse" } } } }, "400": { "description": "Invalid decision for this request" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Review request not found" }, "409": { "description": "Request is no longer pending" } } } }, "/api/admin/article-review-requests/{id}/assignee": { "patch": { "tags": ["article-reviews"], "summary": "Assign an editor to a pending request (or unassign with a null\nassignee). Any reviewer may assign themselves or a colleague.", "operationId": "assign_article_review", "parameters": [ { "name": "id", "in": "path", "description": "Review request ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AssignReviewRequest" } } }, "required": true }, "responses": { "200": { "description": "Assignee updated" }, "400": { "description": "Assignee is not a reviewer" }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Review request not found" }, "409": { "description": "Request is no longer pending" } } } }, "/api/admin/article-reviewers": { "get": { "tags": ["article-reviews"], "summary": "Users holding a reviewer role, for the assignment dropdown.", "operationId": "list_article_reviewers", "responses": { "200": { "description": "Assignable reviewers", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReviewerListResponse" } } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" } } } }, "/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/articles/{slug}/quoting": { "put": { "tags": ["articles"], "summary": "Turn the reader's sentence-selection layer on or off for one\narticle. Admin only — authors do not control this.", "operationId": "set_article_quoting", "parameters": [ { "name": "slug", "in": "path", "description": "Article slug", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetArticleQuotingRequest" } } }, "required": true }, "responses": { "200": { "description": "Setting updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleQuotingResponse" } } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Article not found" } } } }, "/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" }, "403": { "description": "Insufficient permissions" } } } }, "/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" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Not found" } } }, "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" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Not found" } } } }, "/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/series": { "get": { "tags": ["admin"], "summary": "List series with member counts for the manage page", "operationId": "admin_list_series", "responses": { "200": { "description": "Series with member counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SeriesAdminListResponse" } } } }, "403": { "description": "Insufficient permissions" } } }, "post": { "tags": ["admin"], "summary": "Create a series; the slug is generated from the name and immutable", "operationId": "create_series", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSeriesRequest" } } }, "required": true }, "responses": { "200": { "description": "Series created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SeriesAdminResponse" } } } }, "400": { "description": "Invalid or duplicate name" }, "403": { "description": "Insufficient permissions" } } } }, "/api/admin/series/{id}": { "delete": { "tags": ["admin"], "summary": "Delete a series; refused while any article is still attached", "operationId": "delete_series", "parameters": [ { "name": "id", "in": "path", "description": "Series ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Series deleted" }, "400": { "description": "Series still has articles" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Series not found" } } }, "patch": { "tags": ["admin"], "summary": "Update series metadata (name, description, pinned, sort order)", "operationId": "update_series", "parameters": [ { "name": "id", "in": "path", "description": "Series ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateSeriesRequest" } } }, "required": true }, "responses": { "200": { "description": "Series updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SeriesAdminResponse" } } } }, "400": { "description": "Invalid or duplicate name" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Series not found" } } } }, "/api/admin/series/{id}/articles": { "get": { "tags": ["admin"], "summary": "List a series' members in order, all statuses (manage drawer)", "operationId": "list_series_members", "parameters": [ { "name": "id", "in": "path", "description": "Series ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Ordered members", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SeriesMemberListResponse" } } } }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Series not found" } } }, "post": { "tags": ["admin"], "summary": "Add a published article to a series (prepends at position 1)", "operationId": "add_series_article", "parameters": [ { "name": "id", "in": "path", "description": "Series ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddSeriesArticleRequest" } } }, "required": true }, "responses": { "200": { "description": "Article added" }, "400": { "description": "Article not published or already in the series" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Series or article not found" } } } }, "/api/admin/series/{id}/articles/order": { "put": { "tags": ["admin"], "summary": "Reorder a series' members (full permutation of current members)", "operationId": "reorder_series_articles", "parameters": [ { "name": "id", "in": "path", "description": "Series ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReorderSeriesArticlesRequest" } } }, "required": true }, "responses": { "200": { "description": "Members reordered" }, "400": { "description": "Not a permutation of the current members" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Series not found" } } } }, "/api/admin/series/{id}/articles/{article_id}": { "delete": { "tags": ["admin"], "summary": "Remove an article from a series", "operationId": "remove_series_article", "parameters": [ { "name": "id", "in": "path", "description": "Series ID", "required": true, "schema": { "type": "string" } }, { "name": "article_id", "in": "path", "description": "Article ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Article removed" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Series, article, or membership not found" } } } }, "/api/admin/topics": { "get": { "tags": ["admin"], "summary": "List topics with per-topic article usage counts", "operationId": "admin_list_topics", "responses": { "200": { "description": "Topics with usage counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TopicAdminListResponse" } } } }, "403": { "description": "Insufficient permissions" } } }, "post": { "tags": ["admin"], "summary": "Create a topic; the slug is generated from the name and immutable", "operationId": "create_topic", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateTopicRequest" } } }, "required": true }, "responses": { "200": { "description": "Topic created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TopicAdminResponse" } } } }, "400": { "description": "Invalid or duplicate name" }, "403": { "description": "Insufficient permissions" } } } }, "/api/admin/topics/{id}": { "delete": { "tags": ["admin"], "summary": "Delete a topic; refused while any article still uses it", "operationId": "delete_topic", "parameters": [ { "name": "id", "in": "path", "description": "Topic ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Topic deleted" }, "400": { "description": "Topic still in use" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Topic not found" } } }, "patch": { "tags": ["admin"], "summary": "Rename a topic (display name only; slug never changes)", "operationId": "update_topic", "parameters": [ { "name": "id", "in": "path", "description": "Topic ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateTopicRequest" } } }, "required": true }, "responses": { "200": { "description": "Topic renamed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TopicAdminResponse" } } } }, "400": { "description": "Invalid or duplicate name" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Topic 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`).", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "q", "in": "query", "description": "Case-insensitive contains-match on the article title.", "required": false, "schema": { "type": ["string", "null"] } }, { "name": "author", "in": "query", "description": "Case-insensitive contains-match on the author's display name or\nhandle.", "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": false, "schema": { "type": "string" } }, { "name": "state", "in": "query", "description": "CSRF state", "required": false, "schema": { "type": "string" } }, { "name": "error", "in": "query", "description": "Provider-side error (e.g. user cancelled)", "required": false, "schema": { "type": "string" } } ], "responses": { "302": { "description": "Redirect to frontend" } } } }, "/api/auth/google": { "get": { "tags": ["auth"], "summary": "Redirect to Google OAuth", "operationId": "google_login", "responses": { "302": { "description": "Redirect to Google" } } } }, "/api/auth/google/callback": { "get": { "tags": ["auth"], "summary": "Handle Google OAuth callback", "operationId": "google_callback", "parameters": [ { "name": "code", "in": "query", "description": "Authorization code", "required": false, "schema": { "type": "string" } }, { "name": "state", "in": "query", "description": "CSRF state", "required": false, "schema": { "type": "string" } }, { "name": "error", "in": "query", "description": "Provider-side error (e.g. user cancelled)", "required": false, "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": "Registration accepted — identical whether or not the email already has an account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } }, "400": { "description": "Invalid input" }, "503": { "description": "Registration temporarily unavailable" } } } }, "/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": { "$ref": "#/components/schemas/SentenceKind" } }, { "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": { "$ref": "#/components/schemas/SentenceKind" } } ], "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/collegia": { "get": { "tags": ["collegia"], "summary": "Browse public collegia (Discover)", "operationId": "discover_collegia", "parameters": [ { "name": "q", "in": "query", "description": "Case-insensitive match against name and description.", "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": "Public collegia", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DiscoverCollegiaResponse" } } } }, "401": { "description": "Not authenticated" } } }, "post": { "tags": ["collegia"], "summary": "Create a collegium; the creator becomes its first admin", "operationId": "create_collegium", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCollegiumRequest" } } }, "required": true }, "responses": { "200": { "description": "Collegium created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CollegiumDetailResponse" } } } }, "400": { "description": "Invalid name or creation limit reached" }, "401": { "description": "Not authenticated" } } } }, "/api/collegia/join/{token}": { "post": { "tags": ["collegia"], "summary": "Join a collegium via an invite link", "operationId": "join_by_token", "parameters": [ { "name": "token", "in": "path", "description": "Invite token", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Joined (or already a member)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JoinByTokenResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "Invalid or revoked invite link" } } } }, "/api/collegia/{slug}": { "get": { "tags": ["collegia"], "summary": "Get a collegium's page (member list for members, steward fields for stewards)", "operationId": "get_collegium", "parameters": [ { "name": "slug", "in": "path", "description": "Collegium slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Collegium detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CollegiumDetailResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "Collegium not found" } } }, "patch": { "tags": ["collegia"], "summary": "Update collegium metadata (name, description, privacy) — collegium stewards only", "operationId": "update_collegium", "parameters": [ { "name": "slug", "in": "path", "description": "Collegium slug", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateCollegiumRequest" } } }, "required": true }, "responses": { "200": { "description": "Collegium updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CollegiumDetailResponse" } } } }, "400": { "description": "Invalid name" }, "403": { "description": "Not a collegium steward" }, "404": { "description": "Collegium not found" } } } }, "/api/collegia/{slug}/invite-token": { "post": { "tags": ["collegia"], "summary": "Generate or rotate the collegium's invite link token — collegium stewards only", "operationId": "rotate_invite_token", "parameters": [ { "name": "slug", "in": "path", "description": "Collegium slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "New invite token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InviteTokenResponse" } } } }, "403": { "description": "Not a collegium steward" }, "404": { "description": "Collegium not found" } } }, "delete": { "tags": ["collegia"], "summary": "Disable the collegium's invite link — collegium stewards only", "operationId": "disable_invite_token", "parameters": [ { "name": "slug", "in": "path", "description": "Collegium slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Invite link disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InviteTokenResponse" } } } }, "403": { "description": "Not a collegium steward" }, "404": { "description": "Collegium not found" } } } }, "/api/collegia/{slug}/join-requests": { "get": { "tags": ["collegia"], "summary": "List pending join requests — collegium stewards only", "operationId": "list_join_requests", "parameters": [ { "name": "slug", "in": "path", "description": "Collegium slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Pending requests", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JoinRequestListResponse" } } } }, "403": { "description": "Not a collegium steward" }, "404": { "description": "Collegium not found" } } }, "post": { "tags": ["collegia"], "summary": "Ask to join a public collegium", "operationId": "create_join_request", "parameters": [ { "name": "slug", "in": "path", "description": "Collegium slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Request filed" }, "400": { "description": "Already a member or daily limit reached" }, "404": { "description": "Collegium not found" }, "409": { "description": "A request is already pending" } } } }, "/api/collegia/{slug}/join-requests/{id}": { "patch": { "tags": ["collegia"], "summary": "Approve or reject a join request — collegium stewards only", "operationId": "decide_join_request", "parameters": [ { "name": "slug", "in": "path", "description": "Collegium slug", "required": true, "schema": { "type": "string" } }, { "name": "id", "in": "path", "description": "Join request ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DecideJoinRequestRequest" } } }, "required": true }, "responses": { "200": { "description": "Request decided" }, "400": { "description": "Invalid status" }, "403": { "description": "Not a collegium steward" }, "404": { "description": "Collegium or request not found" }, "409": { "description": "Request already decided" } } } }, "/api/collegia/{slug}/members/{user_id}": { "delete": { "tags": ["collegia"], "summary": "Leave a collegium (self) or remove a member (collegium stewards)", "operationId": "remove_member", "parameters": [ { "name": "slug", "in": "path", "description": "Collegium slug", "required": true, "schema": { "type": "string" } }, { "name": "user_id", "in": "path", "description": "Member's user ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Member removed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RemoveMemberResponse" } } } }, "400": { "description": "Last steward must promote a successor first" }, "403": { "description": "Not allowed to remove this member" }, "404": { "description": "Collegium or member not found" } } }, "patch": { "tags": ["collegia"], "summary": "Promote a member to admin, or step down as steward (self only)", "operationId": "update_member_role", "parameters": [ { "name": "slug", "in": "path", "description": "Collegium slug", "required": true, "schema": { "type": "string" } }, { "name": "user_id", "in": "path", "description": "Member's user ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateMemberRequest" } } }, "required": true }, "responses": { "200": { "description": "Role updated" }, "400": { "description": "Invalid role or last-steward violation" }, "403": { "description": "Not allowed to change this role" }, "404": { "description": "Collegium or member not found" } } } }, "/api/collegia/{slug}/review-requests": { "get": { "tags": ["article-reviews"], "summary": "A collegium's workshop queue: pending review requests submitted to it.\nGroup members only (404 otherwise, so private collegia don't leak).", "operationId": "list_collegium_review_queue", "parameters": [ { "name": "slug", "in": "path", "description": "Collegium slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Pending collegium review requests", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewQueueResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "Collegium 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/review/articles/{article_id}/activity": { "get": { "tags": ["article-reviews"], "summary": "Change-detection stamp for the review page's polling loop. The\nclient compares the whole payload and invalidates its request,\ncomment, and message queries when it changes.", "operationId": "get_review_activity", "parameters": [ { "name": "article_id", "in": "path", "description": "Article ID", "required": true, "schema": { "type": "string" } }, { "name": "collegium_id", "in": "query", "required": false, "schema": { "type": ["string", "null"] } } ], "responses": { "200": { "description": "Activity stamp", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewActivityResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "Article not found" } } } }, "/api/review/articles/{article_id}/messages": { "get": { "tags": ["article-reviews"], "summary": "The article's review channel, shared across review rounds.", "operationId": "list_review_messages", "parameters": [ { "name": "article_id", "in": "path", "description": "Article ID", "required": true, "schema": { "type": "string" } }, { "name": "collegium_id", "in": "query", "required": false, "schema": { "type": ["string", "null"] } } ], "responses": { "200": { "description": "Channel messages", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewMessageListResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "Article not found" } } }, "post": { "tags": ["article-reviews"], "summary": "Post to the article's review channel (author or reviewer). The\nchannel exists once the article has been submitted at least once.", "operationId": "create_review_message", "parameters": [ { "name": "article_id", "in": "path", "description": "Article ID", "required": true, "schema": { "type": "string" } }, { "name": "collegium_id", "in": "query", "required": false, "schema": { "type": ["string", "null"] } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateReviewMessageRequest" } } }, "required": true }, "responses": { "200": { "description": "Message posted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewMessageResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "404": { "description": "Article not found" } } } }, "/api/review/comments/{id}": { "patch": { "tags": ["article-reviews"], "summary": "Resolve or reopen a comment thread (reviewers only).", "operationId": "update_review_comment", "parameters": [ { "name": "id", "in": "path", "description": "Comment ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateReviewCommentRequest" } } }, "required": true }, "responses": { "200": { "description": "Comment updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewCommentResponse" } } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Insufficient permissions" }, "404": { "description": "Comment not found" } } } }, "/api/review/comments/{id}/replies": { "post": { "tags": ["article-reviews"], "summary": "Reply within a comment thread (author or reviewer).", "operationId": "create_review_reply", "parameters": [ { "name": "id", "in": "path", "description": "Comment ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateReviewReplyRequest" } } }, "required": true }, "responses": { "200": { "description": "Reply created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewCommentResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "404": { "description": "Comment not found" } } } }, "/api/review/requests/{id}": { "get": { "tags": ["article-reviews"], "summary": "Review page payload: the request, its frozen snapshot, article\nmetadata, and sibling rounds. Author or reviewer only (404 otherwise).", "operationId": "get_review_request", "parameters": [ { "name": "id", "in": "path", "description": "Review request ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Review detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewDetailResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "Review request not found" } } } }, "/api/review/requests/{id}/comments": { "get": { "tags": ["article-reviews"], "summary": "List the comment threads on a request's snapshot.", "operationId": "list_review_comments", "parameters": [ { "name": "id", "in": "path", "description": "Review request ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Comment threads", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewCommentListResponse" } } } }, "401": { "description": "Not authenticated" }, "404": { "description": "Review request not found" } } }, "post": { "tags": ["article-reviews"], "summary": "Create an anchored comment on a request's snapshot (reviewers only).", "operationId": "create_review_comment", "parameters": [ { "name": "id", "in": "path", "description": "Review request ID", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateReviewCommentRequest" } } }, "required": true }, "responses": { "200": { "description": "Comment created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewCommentResponse" } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Not authenticated" }, "404": { "description": "Review request not found" } } } }, "/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/series": { "get": { "tags": ["series"], "summary": "List series, pinned first, then by editor-set sort order", "operationId": "list_series", "parameters": [ { "name": "pinned", "in": "query", "description": "`true` restricts the list to pinned series.", "required": false, "schema": { "type": ["boolean", "null"] } } ], "responses": { "200": { "description": "All series", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SeriesListResponse" } } } } } } }, "/api/series/{slug}": { "get": { "tags": ["series"], "summary": "Get a series with one page of its published articles", "operationId": "get_series", "parameters": [ { "name": "slug", "in": "path", "description": "Series slug", "required": true, "schema": { "type": "string" } }, { "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": "Series detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SeriesDetailResponse" } } } }, "404": { "description": "Series 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": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SourceType" } ] } }, { "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/user/articles/{slug}/review-requests": { "post": { "tags": ["article-reviews"], "summary": "Submit an article for review (feedback or publication hand-off).", "operationId": "create_review_request", "parameters": [ { "name": "slug", "in": "path", "description": "Article slug", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateReviewRequestRequest" } } }, "required": true }, "responses": { "200": { "description": "Review request created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArticleReviewRequestResponse" } } } }, "400": { "description": "Invalid input or article not eligible" }, "401": { "description": "Not authenticated" }, "404": { "description": "Article not found" }, "409": { "description": "A review request is already pending" } } } }, "/api/user/collegia": { "get": { "tags": ["collegia"], "summary": "List the collegiums the current user belongs to", "operationId": "list_my_collegia", "responses": { "200": { "description": "My collegia", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MyCollegiaResponse" } } } }, "401": { "description": "Not authenticated" } } } }, "/api/user/review-requests/{id}/resolve": { "post": { "tags": ["article-reviews"], "summary": "Close a pending collegium-review round. Who may close follows the\nround's visibility snapshot: writing-collegium rounds (member-visible)\nclose by the author or a collegium steward; classroom rounds (stewards-only)\nclose by a collegium steward alone — the student can only withdraw.\nEditorial rounds are decided by editors instead.", "operationId": "resolve_review_request", "parameters": [ { "name": "id", "in": "path", "description": "Review request ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Request resolved" }, "401": { "description": "Not authenticated" }, "404": { "description": "No pending collegium review request found" } } } }, "/api/user/review-requests/{id}/withdraw": { "post": { "tags": ["article-reviews"], "summary": "Withdraw a pending review request (author only). Withdrawing the\narticle's only request revokes editor access to the draft.", "operationId": "withdraw_review_request", "parameters": [ { "name": "id", "in": "path", "description": "Review request ID", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Request withdrawn" }, "401": { "description": "Not authenticated" }, "404": { "description": "No pending request 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." } ] } } }, "AddSeriesArticleRequest": { "type": "object", "required": ["article_id"], "properties": { "article_id": { "type": "string" } } }, "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", "quoting_disabled", "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" } }, "latest_review_request_id": { "type": ["string", "null"], "description": "Most recent review request of any status (owner only) — the\nauthor's entry point to the review page and its history." }, "markdown": { "type": "string" }, "pending_review_request_id": { "type": ["string", "null"], "description": "Pending review request on this article, if any. Populated for the\nowner (`get_user_article`) only; absent on public endpoints." }, "published_at": { "type": ["string", "null"] }, "quoting_disabled": { "type": "boolean", "description": "Admin-set: suppress the reader's sentence-selection layer, for\narticles that read as blog posts rather than quotable scholarship." }, "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." }, "series": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleSeriesContext" }, "description": "Series this article belongs to, with published prev/next\nneighbors — drives the article-page series strip. Empty for\narticles in no series (and on owner-facing endpoints)." }, "slug": { "type": "string" }, "status": { "$ref": "#/components/schemas/ArticleStatus" }, "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" } } }, "ArticleQuotingResponse": { "type": "object", "required": ["slug", "quoting_disabled"], "properties": { "quoting_disabled": { "type": "boolean" }, "slug": { "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`." }, "pending_review_request_id": { "type": ["string", "null"], "description": "Pending review request on this article, if any. Owner-facing\nlistings only; absent on public endpoints." }, "published_at": { "type": ["string", "null"] }, "slug": { "type": "string" }, "status": { "$ref": "#/components/schemas/ArticleStatus" }, "title": { "type": "string" }, "topics": { "type": "array", "items": { "$ref": "#/components/schemas/TopicResponse" } }, "updated_at": { "type": "string" } } }, "ArticleReviewActivityResponse": { "type": "object", "description": "A cheap change-detection stamp for the review page's polling: the\nclient treats the whole object as an opaque token and invalidates\nits queries when any field changes. Counts alone would miss\nun-resolving a comment; timestamps alone would miss deletions.", "required": [ "messages", "comments", "resolved_comments", "requests_updated_at" ], "properties": { "comments": { "type": "integer", "format": "int64" }, "messages": { "type": "integer", "format": "int64" }, "requests_updated_at": { "type": "string" }, "resolved_comments": { "type": "integer", "format": "int64" } } }, "ArticleReviewCommentListResponse": { "type": "object", "required": ["comments"], "properties": { "comments": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleReviewCommentResponse" } } } }, "ArticleReviewCommentResponse": { "type": "object", "required": ["id", "request_id", "body", "created_at"], "properties": { "block_index": { "type": ["integer", "null"], "format": "int32" }, "body": { "type": "string" }, "created_at": { "type": "string" }, "id": { "type": "string" }, "parent_id": { "type": ["string", "null"] }, "quoted_text": { "type": ["string", "null"] }, "request_id": { "type": "string" }, "resolved_at": { "type": ["string", "null"] }, "sender": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ReviewParticipant" } ] }, "sentence_end": { "type": ["integer", "null"], "format": "int32" }, "sentence_start": { "type": ["integer", "null"], "format": "int32" } } }, "ArticleReviewDetailResponse": { "type": "object", "required": [ "request", "snapshot_html", "article", "requests", "draft_changed" ], "properties": { "article": { "$ref": "#/components/schemas/ReviewArticleMeta" }, "assignee": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ReviewParticipant", "description": "Editor currently assigned to this request, if any." } ] }, "collegium": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ReviewCollegiumMeta", "description": "The collegium reviewing this request; absent for editorial rounds." } ] }, "draft_changed": { "type": "boolean", "description": "True when the article body was edited after this request was\nsubmitted — the snapshot no longer matches the live draft." }, "request": { "$ref": "#/components/schemas/ArticleReviewRequestResponse" }, "requests": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleReviewRequestResponse" }, "description": "Every review round for this article (withdrawn rounds only for\nthe author), newest first — for round navigation on the page." }, "snapshot_html": { "type": "string", "description": "The article HTML frozen at submission, annotated with\n`data-block` / `data-s` markers that comments anchor into." } } }, "ArticleReviewMessageListResponse": { "type": "object", "required": ["messages"], "properties": { "messages": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleReviewMessageResponse" } } } }, "ArticleReviewMessageResponse": { "type": "object", "required": ["id", "body", "created_at"], "properties": { "body": { "type": "string" }, "created_at": { "type": "string" }, "id": { "type": "string" }, "sender": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ReviewParticipant" } ] } } }, "ArticleReviewQueueItem": { "type": "object", "required": [ "id", "article_id", "article_title", "article_slug", "article_status", "author_user_id", "author_display_name", "intent", "status", "submitted_at", "open_comment_count" ], "properties": { "article_id": { "type": "string" }, "article_slug": { "type": "string" }, "article_status": { "$ref": "#/components/schemas/ArticleStatus" }, "article_title": { "type": "string" }, "assignee": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ReviewParticipant" } ] }, "author_display_name": { "type": "string" }, "author_handle": { "type": ["string", "null"] }, "author_user_id": { "type": "string" }, "id": { "type": "string" }, "intent": { "$ref": "#/components/schemas/ReviewIntent" }, "open_comment_count": { "type": "integer", "format": "int64", "description": "Unresolved top-level comments on this request." }, "resolved_at": { "type": ["string", "null"] }, "status": { "$ref": "#/components/schemas/ReviewRequestStatus" }, "submitted_at": { "type": "string" } } }, "ArticleReviewQueueResponse": { "type": "object", "required": ["items", "total"], "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleReviewQueueItem" } }, "total": { "type": "integer", "format": "int64" } } }, "ArticleReviewRequestResponse": { "type": "object", "required": [ "id", "article_id", "intent", "status", "submitted_at" ], "properties": { "article_id": { "type": "string" }, "collegium_id": { "type": ["string", "null"], "description": "Review audience; absent for editorial rounds." }, "collegium_name": { "type": ["string", "null"] }, "id": { "type": "string" }, "intent": { "$ref": "#/components/schemas/ReviewIntent" }, "resolved_at": { "type": ["string", "null"] }, "status": { "$ref": "#/components/schemas/ReviewRequestStatus" }, "submitted_at": { "type": "string" } } }, "ArticleSeriesContext": { "type": "object", "description": "A series the article belongs to, as shown on the article page strip.\nNeighbors are published members only; `prev` is toward position 1\n(the newest end for prepend-ordered series like the blog).", "required": ["name", "slug"], "properties": { "name": { "type": "string" }, "next": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SeriesNeighbor" } ] }, "prev": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SeriesNeighbor" } ] }, "slug": { "type": "string" } } }, "ArticleStatus": { "type": "string", "description": "An article's lifecycle state. Stored as the Postgres enum\n`article_status`.", "enum": ["draft", "published", "archived"] }, "AssignReviewRequest": { "type": "object", "properties": { "assignee_id": { "type": ["string", "null"], "description": "Editor to assign, or null/omitted to unassign." } } }, "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", "kind"], "properties": { "book_slug": { "type": "string" }, "end_id": { "type": ["string", "null"] }, "end_number": { "type": ["integer", "null"], "format": "int32" }, "kind": { "$ref": "#/components/schemas/SentenceKind" }, "node_slug": { "type": "string" }, "start_id": { "type": ["string", "null"] }, "start_number": { "type": ["integer", "null"], "format": "int32", "description": "Body/footnote sentence-number addressing. Optional when `start_id`\naddresses the range by sentence UUID instead (quotations anchored\non unnumbered sentences: figure captions, headings)." } } }, "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`." }, "figure_html": { "type": ["string", "null"], "description": "For a quotation anchored on a figure's caption: the figure block's\nverbatim `
` markup, so the embed renders the whole figure\nexactly as the reader does." }, "figure_number": { "type": ["integer", "null"], "format": "int32", "description": "The figure's book-wide display number — the reader deep-link key is\n`fig{N}`." }, "figure_original_html": { "type": ["string", "null"] }, "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", "has_original_layer" ], "properties": { "author": { "type": "string" }, "has_original_layer": { "type": "boolean", "description": "Whether this book carries a second, original-orthography layer\n(`content_blocks.original_html`). False for editions curated as a\nsingle layer — a text already in modern spelling, where a reviewed\nlayer would duplicate the reading text (peirce1). The reader hides\nits orthography toggle when this is false, rather than offering a\nswitch that changes nothing." }, "id": { "type": "string" }, "language": { "type": "string" }, "publication_year": { "type": ["integer", "null"], "format": "int32", "description": "Identity year (`COALESCE(original_year, publication_year)`) —\nthe edition this text presents, which is also what SEO metadata\npublishes. The truthful printing year lives on the source record\n(book-about endpoint)." }, "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" } } }, "CollegiumDetailResponse": { "type": "object", "description": "Full collegium page payload. Member-only and admin-only fields are absent\nfor viewers below that tier.", "required": [ "id", "name", "slug", "is_private", "member_count", "my_pending_request", "review_visibility", "created_at" ], "properties": { "created_at": { "type": "string" }, "description": { "type": ["string", "null"] }, "id": { "type": "string" }, "invite_token": { "type": ["string", "null"], "description": "Admins only; absent for stewards too when no invite link is active\n(`my_role` tells the frontend whether the field is authoritative)." }, "is_private": { "type": "boolean" }, "member_count": { "type": "integer", "format": "int64" }, "members": { "type": ["array", "null"], "items": { "$ref": "#/components/schemas/CollegiumMemberResponse" }, "description": "Members only." }, "my_pending_request": { "type": "boolean" }, "my_role": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/CollegiumRole" } ] }, "name": { "type": "string" }, "pending_join_request_count": { "type": ["integer", "null"], "format": "int64", "description": "Admins only." }, "review_visibility": { "$ref": "#/components/schemas/ReviewVisibility" }, "slug": { "type": "string" } } }, "CollegiumMemberResponse": { "type": "object", "required": ["user_id", "display_name", "role", "joined_at"], "properties": { "display_name": { "type": "string" }, "handle": { "type": ["string", "null"] }, "joined_at": { "type": "string" }, "role": { "$ref": "#/components/schemas/CollegiumRole" }, "user_id": { "type": "string" } } }, "CollegiumResponse": { "type": "object", "description": "A collegium as shown on list surfaces (My collegia, Discover). `my_role`\nand `my_pending_request` are relative to the requesting user.", "required": [ "id", "name", "slug", "is_private", "member_count", "my_pending_request", "review_visibility", "created_at" ], "properties": { "created_at": { "type": "string" }, "description": { "type": ["string", "null"] }, "id": { "type": "string" }, "is_private": { "type": "boolean" }, "member_count": { "type": "integer", "format": "int64" }, "my_pending_request": { "type": "boolean" }, "my_role": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/CollegiumRole", "description": "Absent for non-members." } ] }, "name": { "type": "string" }, "review_visibility": { "$ref": "#/components/schemas/ReviewVisibility" }, "slug": { "type": "string" } } }, "CollegiumRole": { "type": "string", "description": "A member's standing in a collegium. Stored as the Postgres enum\n`collegium_member_role`; stewards run the collegium (settings,\nmembership, invite links, closing rounds).", "enum": ["steward", "member"] }, "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." } } }, "CreateCollegiumRequest": { "type": "object", "required": ["name"], "properties": { "description": { "type": ["string", "null"] }, "is_private": { "type": ["boolean", "null"] }, "name": { "type": "string" }, "review_visibility": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ReviewVisibility", "description": "Defaults to `members`." } ] } } }, "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": { "$ref": "#/components/schemas/SentenceKind" }, "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": { "$ref": "#/components/schemas/ResourceType" }, "scope": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ResourceScope", "description": "\"work\" (default) | \"language\" | \"edition\" — see ResourceResponse.scope." } ] }, "sentence_end": { "type": ["integer", "null"], "format": "int32" }, "sentence_kind": { "$ref": "#/components/schemas/SentenceKind" }, "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": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/VerbatimKind" } ] } } }, "CreateReviewCommentRequest": { "type": "object", "required": ["block_index", "body"], "properties": { "block_index": { "type": "integer", "format": "int32", "description": "`data-block` index of the snapshot block this comment anchors to." }, "body": { "type": "string" }, "quoted_text": { "type": ["string", "null"], "description": "The anchored text as the editor saw it, for the comment rail." }, "sentence_end": { "type": ["integer", "null"], "format": "int32" }, "sentence_start": { "type": ["integer", "null"], "format": "int32", "description": "Sentence range within the block (`data-s` indices, inclusive).\nOmit both for a block-level comment (embeds, figures)." } } }, "CreateReviewMessageRequest": { "type": "object", "required": ["body"], "properties": { "body": { "type": "string" } } }, "CreateReviewReplyRequest": { "type": "object", "required": ["body"], "properties": { "body": { "type": "string" } } }, "CreateReviewRequestRequest": { "type": "object", "required": ["intent"], "properties": { "collegium_id": { "type": ["string", "null"], "description": "Review audience: a collegium the author belongs to, or absent for the\neditorial team. Collegium reviews are feedback-only." }, "intent": { "$ref": "#/components/schemas/ReviewIntent", "description": "`feedback` — the author wants comments; the reviewing side closes\nthe request as `resolved`. `publication` — a hand-off to the\neditors: approving publishes the article (if still a draft) and\napplies the `imprimatur` label." }, "message": { "type": ["string", "null"], "description": "Optional note posted as the first message in the article's\nreview channel." } } }, "CreateSeriesRequest": { "type": "object", "description": "`slug` is generated from the name at creation and immutable after —\nit appears in shareable URLs (`/articles/series/`).", "required": ["name"], "properties": { "description": { "type": ["string", "null"] }, "name": { "type": "string" } } }, "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"] }, "original_year": { "type": ["integer", "null"], "format": "int32" }, "page_end": { "type": ["integer", "null"], "format": "int32" }, "page_start": { "type": ["integer", "null"], "format": "int32" }, "parent_source_id": { "type": ["string", "null"] }, "publication_place": { "type": ["string", "null"] }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "publisher": { "type": ["string", "null"] }, "source_type": { "$ref": "#/components/schemas/SourceType" }, "title": { "type": "string" }, "title_display": { "type": ["string", "null"] }, "translation_of_id": { "type": ["string", "null"] }, "url": { "type": ["string", "null"] }, "volume": { "type": ["string", "null"] } } }, "CreateTopicRequest": { "type": "object", "description": "`slug` is generated from the name at creation and immutable after —\nit appears in shareable URLs (`/articles?topic_slug=…`).", "required": ["name"], "properties": { "name": { "type": "string" } } }, "DecideJoinRequestRequest": { "type": "object", "required": ["status"], "properties": { "status": { "$ref": "#/components/schemas/JoinRequestDecision" } } }, "DiscoverCollegiaResponse": { "type": "object", "required": ["collegia", "total"], "properties": { "collegia": { "type": "array", "items": { "$ref": "#/components/schemas/CollegiumResponse" } }, "total": { "type": "integer", "format": "int64" } } }, "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" } } }, "InviteTokenResponse": { "type": "object", "properties": { "invite_token": { "type": ["string", "null"] } } }, "JoinByTokenResponse": { "type": "object", "description": "Result of redeeming an invite link; the frontend navigates to the\ncollegium page at `slug`.", "required": ["slug", "name", "already_member"], "properties": { "already_member": { "type": "boolean" }, "name": { "type": "string" }, "slug": { "type": "string" } } }, "JoinRequestDecision": { "type": "string", "enum": ["approved", "rejected"] }, "JoinRequestListResponse": { "type": "object", "required": ["requests"], "properties": { "requests": { "type": "array", "items": { "$ref": "#/components/schemas/JoinRequestResponse" } } } }, "JoinRequestResponse": { "type": "object", "required": ["id", "user_id", "display_name", "created_at"], "properties": { "created_at": { "type": "string" }, "display_name": { "type": "string" }, "handle": { "type": ["string", "null"] }, "id": { "type": "string" }, "user_id": { "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", "text_length", "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"] }, "text_length": { "type": "integer", "format": "int64" }, "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", "description": "Identity year of the work — the edition presented\n(`sources.original_year`), falling back to the printing's\n`publication_year`. E.g. 1787 for the Kritik read in the 1911\nAkademie-Ausgabe." }, "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": { "$ref": "#/components/schemas/SourcePersonRole" } } }, "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" } } }, "MarginNoteResponse": { "type": "object", "description": "Authorial margin note rendered in the reader gutter beside its anchor\nsentence (no in-text marker, unlike footnotes).", "required": ["id", "number", "sentences"], "properties": { "id": { "type": "string" }, "number": { "type": "integer", "format": "int32" }, "sentences": { "type": "array", "items": { "$ref": "#/components/schemas/MarginNoteSentenceResponse" } } } }, "MarginNoteSentenceResponse": { "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" } } }, "MessageResponse": { "type": "object", "required": ["message"], "properties": { "message": { "type": "string" } } }, "MyCollegiaResponse": { "type": "object", "required": ["collegia", "created_count", "max_created"], "properties": { "collegia": { "type": "array", "items": { "$ref": "#/components/schemas/CollegiumResponse" } }, "created_count": { "type": "integer", "format": "int64", "description": "Lifetime collegia created (soft-deleted ones count)." }, "max_created": { "type": "integer", "format": "int64", "description": "Lifetime creation cap for this user's tier." } } }, "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": { "$ref": "#/components/schemas/SentenceKind" }, "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"] }, "margin_prefix": { "type": ["string", "null"], "description": "Margin display prefix, concatenated verbatim with `ref_value`\n(\"B \" → \"B 344\", \"9.\" → \"9.53\"); absent = bare number." }, "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": { "$ref": "#/components/schemas/SourceType" }, "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": { "$ref": "#/components/schemas/SentenceKind" }, "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", "anchor_sentence_start_id", "sentence_kind", "has_source_view", "note_count", "created_at" ], "properties": { "anchor_block_type": { "type": ["string", "null"], "description": "Block type hosting the start anchor (\"paragraph\" | \"figure\" |\n\"heading\" | …). None for footnote/margin-note anchors." }, "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_id": { "type": ["string", "null"] }, "anchor_sentence_end_number": { "type": ["integer", "null"], "format": "int32" }, "anchor_sentence_start_id": { "type": "string", "description": "Anchor sentence UUIDs — the stable address for sentences that carry\nno `sentence_number` (figure captions, headings). The article embed\ndirective uses these when `anchor_block_type` is figure/heading." }, "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": { "$ref": "#/components/schemas/SentenceKind" }, "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" }, "turnstile_token": { "type": ["string", "null"], "description": "Cloudflare Turnstile response token. Required (and verified\nserver-side) only when the deployment has the bot gate enabled;\nignored otherwise." } } }, "RemoveMemberResponse": { "type": "object", "description": "Membership removal result; `collegium_deleted` is true when the last\nmember left and the collegium was soft-deleted.", "required": ["collegium_deleted"], "properties": { "collegium_deleted": { "type": "boolean" } } }, "ReorderSeriesArticlesRequest": { "type": "object", "description": "The full member list in its new order — must be an exact permutation\nof the series' current members.", "required": ["article_ids"], "properties": { "article_ids": { "type": "array", "items": { "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": { "$ref": "#/components/schemas/ResourceType" }, "review_status": { "$ref": "#/components/schemas/ResourceReviewStatus", "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": { "$ref": "#/components/schemas/ResourceScope", "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": { "$ref": "#/components/schemas/SentenceKind" }, "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": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/VerbatimKind" } ] } } }, "ResourceReviewStatus": { "type": "string", "description": "Review state of a resource that entered via community submission.", "enum": ["pending", "approved", "rejected"] }, "ResourceScope": { "type": "string", "description": "Stored as the Postgres enum `resource_scope` (ADR 0008).", "enum": ["work", "language", "edition"] }, "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" } } }, "ResourceType": { "type": "string", "description": "Stored as the Postgres enum `resource_type`.", "enum": ["verbatim", "paraphrase", "allusion"] }, "ReviewArticleMeta": { "type": "object", "description": "Article metadata shown on the review page. Distinct from the public\narticle DTOs because the article may still be a draft here.", "required": [ "id", "title", "slug", "status", "author_user_id", "author_display_name", "labels" ], "properties": { "author_display_name": { "type": "string" }, "author_handle": { "type": ["string", "null"] }, "author_user_id": { "type": "string" }, "id": { "type": "string" }, "labels": { "type": "array", "items": { "$ref": "#/components/schemas/EditorialLabelResponse" } }, "slug": { "type": "string" }, "status": { "$ref": "#/components/schemas/ArticleStatus" }, "title": { "type": "string" } } }, "ReviewCollegiumMeta": { "type": "object", "description": "The reviewing collegium, for the review page header, links, and action\ngating.", "required": ["id", "name", "slug", "member_visible"], "properties": { "id": { "type": "string" }, "member_visible": { "type": "boolean", "description": "This round's visibility snapshot: true = all members review\n(writing-collegium mode), false = stewards only (classroom mode)." }, "my_role": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/CollegiumRole", "description": "The viewer's role in the collegium; absent for non-members\n(e.g. the author after leaving the collegium)." } ] }, "name": { "type": "string" }, "slug": { "type": "string" } } }, "ReviewDecision": { "type": "string", "description": "The subset of statuses an editor may decide a pending request into\n(`pending`/`withdrawn` are unrepresentable by construction). Binds to\nthe same Postgres enum as `ReviewRequestStatus`.", "enum": ["approved", "declined", "resolved"] }, "ReviewDecisionRequest": { "type": "object", "required": ["status"], "properties": { "message": { "type": ["string", "null"], "description": "Optional message posted to the article's review channel alongside\nthe decision." }, "status": { "$ref": "#/components/schemas/ReviewDecision", "description": "`approved` | `declined` (publication intent) or `resolved`\n(feedback intent)." } } }, "ReviewIntent": { "type": "string", "description": "What the author asks of the reviewing side. Stored as the Postgres\nenum `article_review_intent`.", "enum": ["feedback", "publication"] }, "ReviewParticipant": { "type": "object", "description": "Who wrote a message or comment. Absent when the account was deleted.", "required": ["user_id", "display_name"], "properties": { "display_name": { "type": "string" }, "handle": { "type": ["string", "null"] }, "user_id": { "type": "string" } } }, "ReviewRequestStatus": { "type": "string", "description": "A review request's lifecycle state. Stored as the Postgres enum\n`article_review_request_status`.", "enum": [ "pending", "approved", "declined", "resolved", "withdrawn" ] }, "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" } } }, "ReviewVisibility": { "type": "string", "description": "Who reviews submissions: everyone (`members`, the writing-circle\nmode) or stewards only (`stewards`, the classroom mode). Stored as\nthe Postgres enum `collegium_review_visibility`; governs new\nsubmissions only (each review request snapshots it).", "enum": ["members", "stewards"] }, "ReviewerListResponse": { "type": "object", "required": ["reviewers"], "properties": { "reviewers": { "type": "array", "items": { "$ref": "#/components/schemas/ReviewParticipant" } } } }, "SentenceData": { "type": "object", "required": ["sentence_number", "html"], "properties": { "html": { "type": "string" }, "original_html": { "type": ["string", "null"] }, "sentence_number": { "type": "integer", "format": "int32" } } }, "SentenceKind": { "type": "string", "description": "Stored as the Postgres enum `sentence_kind`.", "enum": ["body", "footnote", "figure"] }, "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" }, "margin_notes": { "type": "array", "items": { "$ref": "#/components/schemas/MarginNoteResponse" } }, "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" } } }, "SeriesAdminListResponse": { "type": "object", "required": ["series"], "properties": { "series": { "type": "array", "items": { "$ref": "#/components/schemas/SeriesAdminResponse" } } } }, "SeriesAdminResponse": { "type": "object", "description": "Series row for the manage page: public fields plus member counts of\nevery status (delete is refused while `article_count` is non-zero).", "required": [ "id", "name", "slug", "pinned", "sort_order", "article_count", "published_count", "created_at", "updated_at" ], "properties": { "article_count": { "type": "integer", "format": "int64", "description": "Members of any status." }, "created_at": { "type": "string" }, "description": { "type": ["string", "null"] }, "id": { "type": "string" }, "name": { "type": "string" }, "pinned": { "type": "boolean" }, "published_count": { "type": "integer", "format": "int64", "description": "Members currently visible on public series surfaces." }, "slug": { "type": "string" }, "sort_order": { "type": "integer", "format": "int32" }, "updated_at": { "type": "string" } } }, "SeriesDetailResponse": { "type": "object", "required": ["id", "name", "slug", "articles", "total"], "properties": { "articles": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleResponse" }, "description": "One page of published members, position-ascending." }, "description": { "type": ["string", "null"] }, "id": { "type": "string" }, "name": { "type": "string" }, "slug": { "type": "string" }, "total": { "type": "integer", "format": "int64", "description": "Total published members across all pages." } } }, "SeriesListResponse": { "type": "object", "required": ["series"], "properties": { "series": { "type": "array", "items": { "$ref": "#/components/schemas/SeriesResponse" } } } }, "SeriesMemberListResponse": { "type": "object", "required": ["articles"], "properties": { "articles": { "type": "array", "items": { "$ref": "#/components/schemas/SeriesMemberResponse" } } } }, "SeriesMemberResponse": { "type": "object", "description": "Ordered member row for the manage drawer. Unlike the public surfaces\nthis includes non-published members (rendered greyed with their\nstatus) — membership survives unpublish.", "required": [ "article_id", "title", "slug", "status", "author_display_name", "position" ], "properties": { "article_id": { "type": "string" }, "author_display_name": { "type": "string" }, "position": { "type": "integer", "format": "int32" }, "slug": { "type": "string" }, "status": { "$ref": "#/components/schemas/ArticleStatus" }, "title": { "type": "string" } } }, "SeriesNeighbor": { "type": "object", "required": ["title", "slug"], "properties": { "slug": { "type": "string" }, "title": { "type": "string" } } }, "SeriesResponse": { "type": "object", "required": ["id", "name", "slug", "pinned", "article_count"], "properties": { "article_count": { "type": "integer", "format": "int64", "description": "Published members only." }, "description": { "type": ["string", "null"] }, "id": { "type": "string" }, "name": { "type": "string" }, "pinned": { "type": "boolean" }, "slug": { "type": "string" } } }, "SetArticleQuotingRequest": { "type": "object", "required": ["quoting_disabled"], "properties": { "quoting_disabled": { "type": "boolean" } } }, "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": { "$ref": "#/components/schemas/SourcePersonRole" }, "sort_name": { "type": ["string", "null"] } } }, "SourcePersonRole": { "type": "string", "description": "Stored as the Postgres enum `source_person_role`.", "enum": ["author", "editor", "translator", "contributor"] }, "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"] }, "original_year": { "type": ["integer", "null"], "format": "int32", "description": "Year of the edition this text presents, when it differs from the\nprinting transcribed (CSL \"original-date\"). NULL = same as\n`publication_year`." }, "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_place": { "type": ["string", "null"] }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "publisher": { "type": ["string", "null"] }, "source_type": { "$ref": "#/components/schemas/SourceType" }, "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": { "$ref": "#/components/schemas/SourceType" }, "title": { "type": "string" } } }, "SourceType": { "type": "string", "description": "Stored as the Postgres enum `source_type`.", "enum": ["book", "article", "chapter", "journal", "web"] }, "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" } } }, "TopicAdminListResponse": { "type": "object", "required": ["topics"], "properties": { "topics": { "type": "array", "items": { "$ref": "#/components/schemas/TopicAdminResponse" } } } }, "TopicAdminResponse": { "type": "object", "description": "Topic row for the admin panel: public fields plus how many articles\ncurrently carry the topic (delete is refused while non-zero).", "required": ["id", "name", "slug", "article_count"], "properties": { "article_count": { "type": "integer", "format": "int64" }, "id": { "type": "string" }, "name": { "type": "string" }, "slug": { "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", "anchor_sentence_start_id", "sentence_kind", "has_source_view", "note_count", "created_at", "source_type" ], "properties": { "anchor_block_type": { "type": ["string", "null"], "description": "See `QuotationWithContextResponse::anchor_block_type`." }, "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_id": { "type": ["string", "null"] }, "anchor_sentence_end_number": { "type": ["integer", "null"], "format": "int32" }, "anchor_sentence_start_id": { "type": "string", "description": "See `QuotationWithContextResponse::anchor_sentence_start_id`." }, "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": { "$ref": "#/components/schemas/SentenceKind" }, "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" } } } }, "UpdateCollegiumRequest": { "type": "object", "description": "Partial update; the slug never changes. An empty-string `description`\nclears it.", "properties": { "description": { "type": ["string", "null"] }, "is_private": { "type": ["boolean", "null"] }, "name": { "type": ["string", "null"] }, "review_visibility": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ReviewVisibility", "description": "Changing it affects future submissions only — each request keeps\nthe visibility it was submitted under." } ] } } }, "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." } ] } } }, "UpdateMemberRequest": { "type": "object", "required": ["role"], "properties": { "role": { "$ref": "#/components/schemas/CollegiumRole", "description": "`steward` promotes (stewards only); `member` steps down (self only)." } } }, "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": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ResourceType" } ] }, "scope": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ResourceScope", "description": "NOT NULL column: plain Option — omit to leave unchanged." } ] }, "sentence_end": { "type": ["integer", "null"], "format": "int32" }, "sentence_kind": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SentenceKind" } ] }, "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": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/VerbatimKind" } ] } } }, "UpdateReviewCommentRequest": { "type": "object", "required": ["resolved"], "properties": { "resolved": { "type": "boolean" } } }, "UpdateSeriesRequest": { "type": "object", "description": "Partial update; the slug never changes. An empty-string `description`\nclears it.", "properties": { "description": { "type": ["string", "null"] }, "name": { "type": ["string", "null"] }, "pinned": { "type": ["boolean", "null"] }, "sort_order": { "type": ["integer", "null"], "format": "int32" } } }, "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"] }, "original_year": { "type": ["integer", "null"], "format": "int32" }, "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_place": { "type": ["string", "null"] }, "publication_year": { "type": ["integer", "null"], "format": "int32" }, "publisher": { "type": ["string", "null"] }, "source_type": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SourceType" } ] }, "title": { "type": ["string", "null"] }, "title_display": { "type": ["string", "null"] }, "translation_of_id": { "type": ["string", "null"] }, "url": { "type": ["string", "null"] }, "volume": { "type": ["string", "null"] } } }, "UpdateTopicRequest": { "type": "object", "description": "Renames the display name only; the slug never changes.", "required": ["name"], "properties": { "name": { "type": "string" } } }, "UserHandleResponse": { "type": "object", "required": ["handle"], "properties": { "handle": { "type": "string" } } }, "VerbatimKind": { "type": "string", "description": "Stored as the Postgres enum `verbatim_kind`.", "enum": ["entirety", "fragmentary"] } } } }