{"openapi":"3.1.0","info":{"title":"AniFillerPedia API","description":"Public read API for AniFillerPedia, a community-editable database of anime filler/canon/mixed episode data. Every read endpoint (series, episodes, citations, contribution history) is public and unauthenticated — no account, no API key, no rate-limit wall for reasonable use. Only the bulk `/export` dump requires an API key, and only write endpoints that need an accountable identity (voting, bulk submission, moderation, admin) require login.\n\nAuth is cookie-based OAuth (GitHub/Discord) with no bearer-token alternative today — see the Authentication section of [docs/API.md](https://github.com/Napandee/AniFillerPedia/blob/master/docs/API.md#authentication) before building a non-browser client against a login-gated endpoint.\n\nSee [docs/API.md](https://github.com/Napandee/AniFillerPedia/blob/master/docs/API.md) for a narrative guide with real example requests, and [CONTRIBUTING.md](https://github.com/Napandee/AniFillerPedia/blob/master/CONTRIBUTING.md) for how the submission/review/voting workflow fits together.","license":{"name":"Code: MIT — Dataset: CC BY-NC-SA 4.0 (see GET /api/v1/license)","url":"https://github.com/Napandee/AniFillerPedia/blob/master/DATA_LICENSE"},"version":"0.1.0"},"paths":{"/api/v1/health":{"get":{"tags":["health"],"summary":"Health","description":"Liveness check — no auth, no database dependency. Used by the\ndeploy pipeline's post-deploy smoke test (pr-validate.yml) and by\nanything else that just needs to know the API process is up.","operationId":"health_api_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Api V1 Health Get"}}}}}}},"/api/v1/version":{"get":{"tags":["health"],"summary":"Version","description":"The currently-deployed API version string.","operationId":"version_api_v1_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Version Api V1 Version Get"}}}}}}},"/api/v1/series/needs-research":{"get":{"tags":["series"],"summary":"List Needs Research","description":"#153: the public \"needs research\" queue — catalog series that need\ncontributor attention, each with why (`never_researched`,\n`status_drift`, or `episode_count_drift`). Public, unauthenticated,\nsame trust level as `GET /series`'s own browse/search.\n\nDeclared before `GET /series/{id_or_slug}` below so \"needs-research\"\nis matched as this literal path, not swallowed by the slug/id\ncatch-all — FastAPI/Starlette route matching is registration-order\ndependent for two routes on the same prefix.","operationId":"list_needs_research_api_v1_series_needs_research_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NeedsResearchListOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/series":{"get":{"tags":["series"],"summary":"List Series","description":"Search/list the series catalog. Public, unauthenticated.\n\n`q` matches title and known synonyms (alternate/romanized/native-script\ntitles). A plain call with no `q` and no external id (`anilist_id`/\n`mal_id`/`anidb_id`) excludes series with zero researched episodes —\nsee docs/API.md for why. Pass `sort=recently_updated` to order by which\nseries had an episode's status most recently approved instead of the\ndefault insertion order.","operationId":"list_series_api_v1_series_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Title or synonym search","title":"Q"},"description":"Title or synonym search"},{"name":"anilist_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anilist Id"}},{"name":"mal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Mal Id"}},{"name":"anidb_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anidb Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(recently_updated)$"},{"type":"null"}],"title":"Sort"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesListOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/series/{id_or_slug}":{"get":{"tags":["series"],"summary":"Get Series","description":"#116: accepts either a numeric series id (legacy URLs — the\nfrontend 301-redirects these to the canonical slug URL, but the API\nitself keeps resolving both indefinitely, since it's a public contract\nother consumers may already depend on) or a slug.\n\n#155: conditional-request support — a caller sending a matching\nIf-None-Match/If-Modified-Since gets a bare 304 instead of the full\nbody; everyone else gets ETag/Last-Modified on the normal 200 so a\nlater poll can do the same. See services.series.get_series_conditional\nfor what the timestamp/ETag are derived from.","operationId":"get_series_api_v1_series__id_or_slug__get","parameters":[{"name":"id_or_slug","in":"path","required":true,"schema":{"type":"string","title":"Id Or Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesDetailOut"}}}},"404":{"description":"No series matches this id or slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/series/{series_id}/episodes":{"get":{"tags":["series"],"summary":"List Series Episodes","description":"#155: same conditional-request support as GET /series/{id} above,\nderived from MAX(episodes.updated_at) for this series instead — see\nservices.episodes.list_episodes_for_series_conditional.","operationId":"list_series_episodes_api_v1_series__series_id__episodes_get","parameters":[{"name":"series_id","in":"path","required":true,"schema":{"type":"integer","title":"Series Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EpisodeOut"},"title":"Response List Series Episodes Api V1 Series  Series Id  Episodes Get"}}}},"404":{"description":"No series matches this id or slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/episodes/{episode_id}":{"get":{"tags":["episodes"],"summary":"Get Episode","description":"Public, unauthenticated. Same shape as one entry from\n`GET /series/{id}/episodes` — the current, live-authoritative status\nfor this episode, not its history (see the /history route below).","operationId":"get_episode_api_v1_episodes__episode_id__get","parameters":[{"name":"episode_id","in":"path","required":true,"schema":{"type":"integer","title":"Episode Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EpisodeOut"}}}},"404":{"description":"No episode matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/episodes/{episode_id}/history":{"get":{"tags":["episodes"],"summary":"Get Episode History","description":"Every contribution ever submitted for this episode — not just the\ncurrent live one — including each one's review outcome and any\ncommunity votes cast on it. Public, unauthenticated: this project\ndoesn't anonymize active contributors, only accounts that have been\ndeleted (see the privacy policy).","operationId":"get_episode_history_api_v1_episodes__episode_id__history_get","parameters":[{"name":"episode_id","in":"path","required":true,"schema":{"type":"integer","title":"Episode Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ContributionHistoryEntry"},"title":"Response Get Episode History Api V1 Episodes  Episode Id  History Get"}}}},"404":{"description":"No episode matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/{provider}/authorize":{"get":{"tags":["auth"],"summary":"Authorize","description":"Starts the login flow: redirects the browser to `provider`'s own\nOAuth consent screen, with a signed, cookie-bound `state` param (see\n`_start_oauth_redirect`). `provider` is `github` or `discord`. This\nroute (and the whole flow it starts) requires a real browser — see\ndocs/API.md's Authentication section for why there's no non-browser\nequivalent today. `next`, if given, must be a same-site relative path;\nit's where the browser lands after `callback` below completes.","operationId":"authorize_api_v1_auth__provider__authorize_get","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"next","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Unknown provider (must be github or discord)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/{provider}/callback":{"get":{"tags":["auth"],"summary":"Callback","description":"The redirect target `provider` sends the browser back to after\nconsent. Verifies the signed `state` against the cookie set by\n`authorize` above (real OAuth CSRF protection, not just signature\nverification — see `_start_oauth_redirect`'s own comment), exchanges\n`code` for the provider's profile, then either logs in/creates a user\nand sets the session cookie (`SESSION_COOKIE_NAME`), or — when this\ncallback is completing a `/settings/link/{provider}` flow instead of a\nplain login — links the provider to the already-signed-in account. If\nthe original `authorize`/`start_link` call included `next`, redirects\nthere (303); otherwise returns the plain JSON body directly.","operationId":"callback_api_v1_auth__provider__callback_get","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"code","in":"query","required":true,"schema":{"type":"string","title":"Code"}},{"name":"state","in":"query","required":true,"schema":{"type":"string","title":"State"}},{"name":"afp_oauth_state","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Afp Oauth State"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Unknown provider (must be github or discord)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"400":{"description":"OAuth state missing/mismatched or invalid/expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"description":"Provider account already linked to a different user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/local/signup":{"post":{"tags":["auth"],"summary":"Local Signup","description":"Real email+password signup — coexists with OAuth login, does not\nreplace it. No email verification in v1 (see the design spec's\nexplicitly-deferred list) — the account is active immediately.","operationId":"local_signup_api_v1_auth_local_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocalSignupIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Local Signup Api V1 Auth Local Signup Post"}}}},"409":{"description":"Email already registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/local/login":{"post":{"tags":["auth"],"summary":"Local Login","operationId":"local_login_api_v1_auth_local_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocalLoginIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Local Login Api V1 Auth Local Login Post"}}}},"401":{"description":"Invalid email or password","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/logout":{"post":{"tags":["auth"],"summary":"Logout","description":"Clears the session cookie. Idempotent — succeeds identically\nwhether or not the caller was actually signed in.","operationId":"logout_api_v1_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Logout Api V1 Auth Logout Post"}}}}}}},"/api/v1/settings/link/{provider}":{"get":{"tags":["settings"],"summary":"Start Link","description":"Starts the same OAuth redirect as `GET /auth/{provider}/authorize`,\nbut for linking an additional provider to the CALLER's already-\nsigned-in account rather than logging in — requires auth. Never\nauto-links by email match; this explicit, authenticated route is the\nonly way an account gains a second linked provider (CLAUDE.md).","operationId":"start_link_api_v1_settings_link__provider__get","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"next","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"Unknown provider (must be github or discord)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/users/me":{"get":{"tags":["users"],"summary":"Read Current User","description":"The signed-in caller's own profile, including #43's `trust_score`\n(the same formula #14's community voting weights votes by) — not\navailable for any other user via this endpoint; see\n`GET /admin/users` for the admin-only listing of everyone's.","operationId":"read_current_user_api_v1_users_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}}},"delete":{"tags":["users"],"summary":"Delete Current User","description":"#29/#18: self-service account deletion, no admin approval gate.\nEvery FK referencing users is ON DELETE SET NULL (schema.sql) — past\ncontributions/votes/citations anonymize automatically, nothing else to\nclean up here. NOT `async with session.begin():` — get_current_user's\nSELECT already autobegan a transaction on this session (the same fix\nnoted throughout this codebase, e.g. routers/contributions.py).","operationId":"delete_current_user_api_v1_users_me_delete","responses":{"204":{"description":"Successful Response"},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}}}},"/api/v1/users/me/export":{"get":{"tags":["users"],"summary":"Export Current User Data","description":"#208 (GDPR Article 15, right of access): bundles the caller's full\nprofile plus every `/mine`-scoped thing they've submitted/voted on\ninto one response. Reuses the exact same repository/service queries\n`GET /users/me`, `GET /contributions/mine`, `GET /contributions/mine/\nvotes`, `GET /series-proposals/mine`, and `GET /synonym-suggestions/\nmine` already expose separately — this is a bundling endpoint, not a\nsecond implementation of any of them.","operationId":"export_current_user_data_api_v1_users_me_export_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserExportOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}}}},"/api/v1/export/request-access":{"post":{"tags":["export"],"summary":"Request Export Access","description":"Issues a new `/export` API key. No auth of its own — this is how\nanyone first gets a key — so it's rate-limited purely by IP rather than\nby an account. `license_accepted` must be true; the returned key is\nshown exactly once and can't be retrieved again (see the response's\nown `note` field).","operationId":"request_export_access_api_v1_export_request_access_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportAccessRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportAccessResponse"}}}},"400":{"description":"license_accepted was not true","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"429":{"description":"Too Many Requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSubmissionRateLimited"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/export":{"get":{"tags":["export"],"summary":"Get Export","description":"The one non-public endpoint in the read API — a full dataset dump\n(every series and episode) plus an embedded attribution manifest, since\na downloaded file is disconnected from these live docs. Requires the\n`X-API-Key` header, obtained from `POST /export/request-access`.","operationId":"get_export_api_v1_export_get","parameters":[{"name":"x-api-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportOut"}}}},"401":{"description":"Missing or invalid/revoked X-API-Key header","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/export/revoke":{"post":{"tags":["export"],"summary":"Revoke Export Access","description":"Self-service revoke-and-forget for the email collected at\n/export/request-access — see services/export.py for why possessing\nthe key is sufficient identity to act on this record.","operationId":"revoke_export_access_api_v1_export_revoke_post","parameters":[{"name":"x-api-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"204":{"description":"Successful Response"},"401":{"description":"Missing X-API-Key header","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"Unknown API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contributions":{"post":{"tags":["contributions"],"summary":"Submit Contribution","description":"Propose a status (canon/filler/mixed) for one episode, with a\ncitation. Anonymous submission is allowed by design (CLAUDE.md) — an\nanonymous caller needs a Turnstile token instead of login; an\nauthenticated one skips Turnstile entirely. Rejected with 409 if the\nepisode already has a pending contribution (#20's one-pending-per-\nepisode rule) — endorse/dispute that one instead via\n`POST /contributions/{id}/vote`.","operationId":"submit_contribution_api_v1_contributions_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContributionCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContributionOut"}}}},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicatePendingContribution"}}},"description":"Conflict"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSubmissionRateLimited"}}},"description":"Too Many Requests"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contributions"],"summary":"List Pending Contributions","description":"#13: the moderation queue — every contribution currently pending\nreview. Moderator/admin/owner only. `review_status` is accepted but\nonly the default `pending` is meaningful right now; anything else\n404s rather than silently returning an empty list, so a caller finds\nout immediately if they typo'd it.","operationId":"list_pending_contributions_api_v1_contributions_get","parameters":[{"name":"review_status","in":"query","required":false,"schema":{"type":"string","default":"pending","title":"Review Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ContributionOut"},"title":"Response List Pending Contributions Api V1 Contributions Get"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"review_status is anything other than 'pending'","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/series/{series_id}/contributions/bulk":{"post":{"tags":["contributions"],"summary":"Submit Bulk Contributions","description":"#80: submit canon/filler/mixed episode RANGES for a series in one\ncall (range notation, e.g. `\"1-44, 48-49\"`), sharing one citation\nacross the whole batch — instead of one `POST /contributions` per\nepisode. Requires login (unlike the single-episode path): one call\nhere can create hundreds of pending contributions at once. Set\n`dry_run: true` to see exactly what would happen (parsed counts,\nwhich episodes would be skipped as already-pending) without writing\nanything.","operationId":"submit_bulk_contributions_api_v1_series__series_id__contributions_bulk_post","parameters":[{"name":"series_id","in":"path","required":true,"schema":{"type":"integer","title":"Series Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkContributionCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkContributionResult"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"This account has been suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No series matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSubmissionRateLimited"}}},"description":"Too Many Requests"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contributions/mine":{"get":{"tags":["contributions"],"summary":"My Contributions","description":"Every contribution the CALLER has submitted, regardless of review\nstatus — requires login (an anonymous submission has no account to\nlist this against).","operationId":"my_contributions_api_v1_contributions_mine_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ContributionOut"},"type":"array","title":"Response My Contributions Api V1 Contributions Mine Get"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}}}},"/api/v1/contributions/mine/votes":{"get":{"tags":["contributions"],"summary":"My Votes","description":"#30: votes-cast counterpart to `/contributions/mine` above — every\nendorse/dispute vote the caller has cast, with enough context (series\ntitle, episode, current resolution) to render without a follow-up\nrequest per row.","operationId":"my_votes_api_v1_contributions_mine_votes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/MyVoteOut"},"type":"array","title":"Response My Votes Api V1 Contributions Mine Votes Get"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}}}},"/api/v1/contributions/bulk-approve":{"post":{"tags":["contributions"],"summary":"Bulk Approve Contributions","description":"#92: approve up to 500 pending contributions in one call. Moderator/\nadmin/owner only. Never fails wholesale on one bad id — each id in\n`ids` gets its own `ok`/`detail` outcome in the result, same per-id\nerror text `POST /contributions/{id}/approve` would give individually\n(e.g. \"not found\" or \"not pending\").","operationId":"bulk_approve_contributions_api_v1_contributions_bulk_approve_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkApproveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkModerationResult"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contributions/bulk-reject":{"post":{"tags":["contributions"],"summary":"Bulk Reject Contributions","description":"Same shape as `POST /contributions/bulk-approve`, but rejecting —\nwith one shared `review_note` reason applied to every id in the batch\nrather than a per-item note. Moderator/admin/owner only.","operationId":"bulk_reject_contributions_api_v1_contributions_bulk_reject_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkRejectRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkModerationResult"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contributions/{contribution_id}/approve":{"post":{"tags":["contributions"],"summary":"Approve Contribution","description":"Directly approve a pending contribution — the moderator half of\n#13's two approval paths (the other being #14's community vote\nthreshold below). Moderator/admin/owner only.","operationId":"approve_contribution_api_v1_contributions__contribution_id__approve_post","parameters":[{"name":"contribution_id","in":"path","required":true,"schema":{"type":"integer","title":"Contribution Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContributionReviewOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No contribution matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"description":"Contribution is not pending review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contributions/{contribution_id}/reject":{"post":{"tags":["contributions"],"summary":"Reject Contribution","description":"Directly reject a pending contribution, with a required\n`review_note` explaining why. Moderator/admin/owner only.","operationId":"reject_contribution_api_v1_contributions__contribution_id__reject_post","parameters":[{"name":"contribution_id","in":"path","required":true,"schema":{"type":"integer","title":"Contribution Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContributionReject"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContributionReviewOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No contribution matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"description":"Contribution is not pending review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contributions/{contribution_id}/withdraw":{"post":{"tags":["contributions"],"summary":"Withdraw Contribution","description":"#149: withdraw your own still-pending contribution — e.g. to fix a\ntypo'd citation before a moderator/vote resolves it, without waiting on\na rejection first. Only the original submitter may withdraw their own\ncontribution, and only while it's still `pending`; find your own\npending contributions via `GET /contributions/mine`. Anonymous\ncontributions cannot be withdrawn through this endpoint — there's no\npersistent identity to check ownership against (see CLAUDE.md /\nservices/contributions.py for why this project doesn't fabricate one).","operationId":"withdraw_contribution_api_v1_contributions__contribution_id__withdraw_post","parameters":[{"name":"contribution_id","in":"path","required":true,"schema":{"type":"integer","title":"Contribution Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContributionReviewOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"You did not submit this contribution","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No contribution matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"description":"Contribution is not pending review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contributions/{contribution_id}/vote":{"post":{"tags":["contributions"],"summary":"Vote On Contribution","description":"#14: the second approval path (CLAUDE.md) — endorse or dispute a\npending contribution. Open to any logged-in user, not just\nmoderators; anonymous voting is not offered, since a vote's value\ncomes from being weighted by the voter's own accountable\n`trust_score`. A submitter cannot vote on their own contribution, and\neach account gets one vote per contribution. Once cumulative weighted\nendorsement crosses the auto-approval threshold, the contribution\npromotes automatically — no moderator click needed.","operationId":"vote_on_contribution_api_v1_contributions__contribution_id__vote_post","parameters":[{"name":"contribution_id","in":"path","required":true,"schema":{"type":"integer","title":"Contribution Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoteCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoteCastOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Cannot vote on your own submitted contribution, or this account has been suspended (#209)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No contribution matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"description":"Contribution is not pending, or you have already voted on it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/series-proposals/check-title":{"get":{"tags":["series-proposals"],"summary":"Check Title For Duplicates","description":"#150: pre-submission duplicate hint — the propose-series form calls\nthis on Title-field blur, same \"possible match\" pattern #165's\nanilist-lookup endpoint uses for the ID-present path. Public/\nunauthenticated (a plain read against our own catalog, same trust\nlevel as GET /series's own search) and never errors on an empty/\nunmatched title — an empty result is a completely normal outcome, not\na client mistake.","operationId":"check_title_for_duplicates_api_v1_series_proposals_check_title_get","parameters":[{"name":"title","in":"query","required":false,"schema":{"type":"string","maxLength":300,"default":"","title":"Title"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimilarSeriesCheckOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/series-proposals":{"post":{"tags":["series-proposals"],"summary":"Submit Series Proposal","description":"Propose a series that isn't in the catalog yet. Anonymous\nsubmission is allowed (needs a Turnstile token instead of login,\nsame as `POST /contributions`). Optionally carries `episode_data`\n(#85) — the same canon/mixed/filler-range shorthand as\n`POST /series/{id}/contributions/bulk` — held on the proposal until a\nmoderator approves it, at which point the series row and the real\nbulk contributions are created together in one transaction; rejecting\nthe proposal discards the attached data.","operationId":"submit_series_proposal_api_v1_series_proposals_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesProposalCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesProposalOut"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSubmissionRateLimited"}}},"description":"Too Many Requests"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["series-proposals"],"summary":"List Pending Series Proposals","description":"The moderation queue for series proposals — mirrors\n`GET /contributions`'s own review_status behavior exactly. Moderator/\nadmin/owner only.","operationId":"list_pending_series_proposals_api_v1_series_proposals_get","parameters":[{"name":"review_status","in":"query","required":false,"schema":{"type":"string","default":"pending","title":"Review Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SeriesProposalOut"},"title":"Response List Pending Series Proposals Api V1 Series Proposals Get"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"review_status is anything other than 'pending'","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/series-proposals/mine":{"get":{"tags":["series-proposals"],"summary":"My Series Proposals","description":"Every series proposal the CALLER has submitted, regardless of\nreview status. Requires login.","operationId":"my_series_proposals_api_v1_series_proposals_mine_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/SeriesProposalOut"},"type":"array","title":"Response My Series Proposals Api V1 Series Proposals Mine Get"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}}}},"/api/v1/series-proposals/bulk-approve":{"post":{"tags":["series-proposals"],"summary":"Bulk Approve Series Proposals","description":"Same shape as `POST /contributions/bulk-approve`, for series\nproposals instead. Moderator/admin/owner only.","operationId":"bulk_approve_series_proposals_api_v1_series_proposals_bulk_approve_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkApproveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkModerationResult"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/series-proposals/bulk-reject":{"post":{"tags":["series-proposals"],"summary":"Bulk Reject Series Proposals","description":"Same shape as `POST /contributions/bulk-reject`, for series\nproposals instead. Moderator/admin/owner only.","operationId":"bulk_reject_series_proposals_api_v1_series_proposals_bulk_reject_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkRejectRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkModerationResult"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/series-proposals/{series_proposal_id}/approve":{"post":{"tags":["series-proposals"],"summary":"Approve Series Proposal","description":"Approves the proposal and creates the real `series` catalog row.\n#85: if the proposal carried attached `episode_data`, this also\ncreates the real bulk contributions against the new series in the\nsame transaction — see `POST /series-proposals` for how that data\ngets attached. Moderator/admin/owner only.","operationId":"approve_series_proposal_api_v1_series_proposals__series_proposal_id__approve_post","parameters":[{"name":"series_proposal_id","in":"path","required":true,"schema":{"type":"integer","title":"Series Proposal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesProposalReviewOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No series proposal matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"description":"Not pending review, or a series with one of this proposal's external IDs (anilist_id/mal_id/anidb_id) already exists in the catalog","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/series-proposals/{series_proposal_id}/reject":{"post":{"tags":["series-proposals"],"summary":"Reject Series Proposal","description":"Rejects the proposal, with a required `review_note` explaining\nwhy. Any attached `episode_data` is discarded for free — it never\nbecame more than JSON on the now-rejected row. Moderator/admin/owner\nonly.","operationId":"reject_series_proposal_api_v1_series_proposals__series_proposal_id__reject_post","parameters":[{"name":"series_proposal_id","in":"path","required":true,"schema":{"type":"integer","title":"Series Proposal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesProposalReject"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesProposalReviewOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No series proposal matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"description":"Series proposal is not pending review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/synonym-suggestions":{"post":{"tags":["synonym-suggestions"],"summary":"Submit Synonym Suggestion","description":"#148: suggest an alternate/dub/regional title for an already-\ncatalogued series. Anonymous submission is allowed (needs a Turnstile\ntoken instead of login, same as `POST /contributions` and\n`POST /series-proposals`) — moderator approval is what promotes it\ninto the live `series_synonyms` table, never a direct write.","operationId":"submit_synonym_suggestion_api_v1_synonym_suggestions_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SynonymSuggestionCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SynonymSuggestionOut"}}}},"404":{"description":"No series matches series_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicatePendingSynonymSuggestion"}}},"description":"Conflict"},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["synonym-suggestions"],"summary":"List Pending Synonym Suggestions","description":"The moderation queue for synonym suggestions — mirrors\n`GET /contributions`/`GET /series-proposals`'s own review_status\nbehavior exactly. Moderator/admin/owner only.","operationId":"list_pending_synonym_suggestions_api_v1_synonym_suggestions_get","parameters":[{"name":"review_status","in":"query","required":false,"schema":{"type":"string","default":"pending","title":"Review Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SynonymSuggestionOut"},"title":"Response List Pending Synonym Suggestions Api V1 Synonym Suggestions Get"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"review_status is anything other than 'pending'","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/synonym-suggestions/mine":{"get":{"tags":["synonym-suggestions"],"summary":"My Synonym Suggestions","description":"Every synonym suggestion the CALLER has submitted, regardless of\nreview status. Requires login (mirrors GET /series-proposals/mine).","operationId":"my_synonym_suggestions_api_v1_synonym_suggestions_mine_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/SynonymSuggestionOut"},"type":"array","title":"Response My Synonym Suggestions Api V1 Synonym Suggestions Mine Get"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}}}},"/api/v1/synonym-suggestions/bulk-approve":{"post":{"tags":["synonym-suggestions"],"summary":"Bulk Approve Synonym Suggestions","description":"Same shape as the contributions/series-proposals bulk-approve\nendpoints. Moderator/admin/owner only.","operationId":"bulk_approve_synonym_suggestions_api_v1_synonym_suggestions_bulk_approve_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkApproveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkModerationResult"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/synonym-suggestions/bulk-reject":{"post":{"tags":["synonym-suggestions"],"summary":"Bulk Reject Synonym Suggestions","operationId":"bulk_reject_synonym_suggestions_api_v1_synonym_suggestions_bulk_reject_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkRejectRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkModerationResult"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/synonym-suggestions/{suggestion_id}/approve":{"post":{"tags":["synonym-suggestions"],"summary":"Approve Synonym Suggestion","description":"Approves the suggestion and inserts the real row into\n`series_synonyms`. Moderator/admin/owner only.","operationId":"approve_synonym_suggestion_api_v1_synonym_suggestions__suggestion_id__approve_post","parameters":[{"name":"suggestion_id","in":"path","required":true,"schema":{"type":"integer","title":"Suggestion Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SynonymSuggestionReviewOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No synonym suggestion matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"description":"Not pending review, or this series already has that exact synonym recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/synonym-suggestions/{suggestion_id}/reject":{"post":{"tags":["synonym-suggestions"],"summary":"Reject Synonym Suggestion","description":"Rejects the suggestion, with a required `review_note`. Moderator/\nadmin/owner only.","operationId":"reject_synonym_suggestion_api_v1_synonym_suggestions__suggestion_id__reject_post","parameters":[{"name":"suggestion_id","in":"path","required":true,"schema":{"type":"integer","title":"Suggestion Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SynonymSuggestionReject"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SynonymSuggestionReviewOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No synonym suggestion matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"description":"Synonym suggestion is not pending review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/anilist-lookup/{anilist_id}":{"get":{"tags":["anilist-lookup"],"summary":"Anilist Lookup","description":"#165: proxies a single lookup to AniList's own public GraphQL API by\nid — used by submission forms to pre-fill a title on blur. Public,\nanonymous-allowed, but rate-limited (30/hour per caller) since an\nunthrottled proxy would let anyone hammer AniList's own API on this\nproject's behalf.","operationId":"anilist_lookup_api_v1_anilist_lookup__anilist_id__get","parameters":[{"name":"anilist_id","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"title":"Anilist Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AniListLookupOut"}}}},"429":{"description":"Too many AniList lookups this hour","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tos":{"get":{"tags":["legal"],"summary":"Terms Of Service","description":"#209: Terms of Service / acceptable-use policy, served as static\nHTML directly by the API (no frontend page to hand-copy the prose\ninto), same pattern as `GET /privacy` above. Covers contributor\nconduct, our right to remove content/reject contributions/terminate\naccess, the account-suspension mechanism (routers/admin.py's\nPATCH /admin/users/{id}/suspension), and a service-level liability\ndisclaimer distinct from DATA_LICENSE's own data-only one.","operationId":"terms_of_service_api_v1_tos_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/v1/privacy":{"get":{"tags":["legal"],"summary":"Privacy Policy","description":"The project's privacy policy, served as static HTML directly by the\nAPI (no frontend page for it) — also the stable public URL Google's\nOAuth verification review (#24) needs to point at.","operationId":"privacy_policy_api_v1_privacy_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/v1/license":{"get":{"tags":["legal"],"summary":"License Info","description":"#21's dedicated attribution endpoint — was decided but never\nactually built until now (found while writing #16's API docs).\nReuses ExportManifest as-is: the bulk export payload (#22) and this\nendpoint describe the exact same license/attribution facts, so one\nmodel backs both rather than maintaining the text twice.","operationId":"license_info_api_v1_license_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportManifest"}}}}}}},"/api/v1/admin/users":{"get":{"tags":["admin"],"summary":"List Users","description":"Every user account, with each one's computed `trust_score` and\napproved/rejected contribution counts. Admin (or owner) only — see\n`GET /users/me` for a caller's own equivalent, available to everyone.","operationId":"list_users_api_v1_admin_users_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserListOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Admin or owner access required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users/{user_id}/role":{"patch":{"tags":["admin"],"summary":"Update User Role","description":"Promotes/demotes a user between `contributor`/`moderator`/`admin`.\nAdmin (or owner) only, with two further owner-tier restrictions\nenforced by the service layer regardless of caller role (#28,\nCLAUDE.md): the owner's own row can never be changed through this\nendpoint by anyone, and only the owner can grant the `admin` role\nitself (a plain admin can still promote/demote between `contributor`\nand `moderator`). `role` deliberately excludes `'owner'` as a valid\nvalue — it is set once at bootstrap and never assignable here.","operationId":"update_user_role_api_v1_admin_users__user_id__role_patch","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleUpdateIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleUpdateOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Admin/owner access required, OR the target is the owner (role immutable), OR only the owner may grant the admin role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No user matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"role is not one of the valid values","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}}}},"/api/v1/admin/users/{user_id}/suspension":{"patch":{"tags":["admin"],"summary":"Update User Suspension","description":"#209: admin/owner-only account suspension — the enforcement half of\nthe ToS (the other half is the `/tos` policy page itself). A suspended\naccount is blocked from submitting contributions/series-proposals/\nsynonym-suggestions and from voting (core/deps.py's\nensure_not_suspended/require_active_user), but NOT from reading or\nfrom exercising GDPR rights on their own account (GET /users/me,\nGET /users/me/export, DELETE /users/me all stay unaffected — this is\naccount-conduct enforcement, not a way to strip someone's own data\nrights). The owner's own row is immune, same as role changes.","operationId":"update_user_suspension_api_v1_admin_users__user_id__suspension_patch","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuspensionUpdateIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuspensionUpdateOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Admin/owner access required, OR the target is the owner (immune to suspension)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"404":{"description":"No user matches this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/vote-clustering-report":{"get":{"tags":["admin"],"summary":"Vote Clustering Report","description":"#203: the Sybil-monitoring tripwire named as an explicit open item\nin CLAUDE.md's #14 decision record — surfaces pairs of accounts that\nhave repeatedly endorsed EACH OTHER's pending contributions, the\ncheapest concrete signal of two colluding accounts combining\n`trust_score` weight to auto-approve each other with no moderator\nclick needed. Not automated anomaly detection or blocking — still\nexplicitly deferred per the #14 decision, unchanged here — a\nmoderator (or admin/owner) runs this periodically and reviews the\nflagged pairs by hand, same \"manual is fine for v1\" pattern as #23's\ncanary/log-review approach. Moderator-accessible (not admin-only, the\nonly endpoint on this router that isn't) since moderators are this\nproject's day-to-day abuse-watchers. `min_reciprocal_count` (default\n2) is a starting heuristic, not tuned against real abuse data — a\nsingle mutual endorsement is common and innocuous in a small\ncommunity; repeated mutual endorsement is the actual signal worth a\nhuman look.","operationId":"vote_clustering_report_api_v1_admin_vote_clustering_report_get","parameters":[{"name":"min_reciprocal_count","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":2,"title":"Min Reciprocal Count"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoteClusteringReportOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Moderator, admin, or owner role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/traffic":{"get":{"tags":["admin"],"summary":"Traffic Rollups","description":"#221: the daily Cloudflare traffic-analytics rollup dashboard's\ndata source — most recent `limit` days, newest first. Admin/owner\nonly (not moderator-accessible like vote-clustering-report above),\nper #219's decision: raw traffic data (request volume, country-level\ngeography) is higher-signal than anything else this project exposes,\nwith no real value to a casual visitor.\n\nAn empty `items` list is a legitimate, expected v1 response — either\nCLOUDFLARE_ANALYTICS_API_TOKEN isn't provisioned yet, or the daily\nworker loop simply hasn't run yet — the frontend renders an explicit\n\"no data yet\" state for this, not an error.","operationId":"traffic_rollups_api_v1_admin_traffic_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":30,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrafficRollupListOut"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"Admin or owner access required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/activity":{"get":{"tags":["activity"],"summary":"List Activity","description":"#154: public, read-only \"recent changes\" feed — every resolved\n(approved/rejected/withdrawn) episode contribution and series\nproposal, newest first. Sourced entirely from the existing\n`contributions`/`series_proposals` audit trail (`reviewed_at`); no new\nwrites, no new table. Public/unauthenticated, same trust level as\n`GET /series`'s own browse — this is read-only history, not the\nmoderation queue (`GET /contributions`, moderator-only, pending-only).","operationId":"list_activity_api_v1_activity_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivityFeedOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/activity/rss":{"get":{"tags":["activity"],"summary":"Activity Rss","description":"RSS 2.0 rendering of the same feed above (Wikipedia \"Recent\nchanges\" / OpenStreetMap changesets precedent, per #154's own issue\ntext) — cheap to add given `list_activity` already returns exactly\nthe rows an RSS item needs. No `offset`: an RSS reader always wants\nthe newest window, not a specific page.","operationId":"activity_rss_api_v1_activity_rss_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"ActivityFeedItem":{"properties":{"event_type":{"type":"string","title":"Event Type"},"id":{"type":"integer","title":"Id"},"review_status":{"type":"string","title":"Review Status"},"resolution_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Method"},"reviewed_at":{"type":"string","format":"date-time","title":"Reviewed At"},"submitted_at":{"type":"string","format":"date-time","title":"Submitted At"},"review_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Note"},"series_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Series Id"},"series_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Series Title"},"series_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Series Slug"},"episode_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Episode Number"},"proposed_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposed Status"},"citation_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Citation Description"},"proposal_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposal Title"},"submitter_display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Submitter Display Name"},"submitter_github_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Submitter Github Id"},"reviewer_display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reviewer Display Name"},"reviewer_github_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reviewer Github Id"}},"type":"object","required":["event_type","id","review_status","resolution_method","reviewed_at","submitted_at","review_note","series_id","series_title","series_slug","episode_number","proposed_status","citation_description","proposal_title","submitter_display_name","submitter_github_id","reviewer_display_name","reviewer_github_id"],"title":"ActivityFeedItem","description":"#154: one resolved event in the public activity feed —\neither a `contribution` (an episode-level status proposal) or a\n`series_proposal` (a proposal to add a new series). Only ever a\n*resolved* row (approved/rejected/withdrawn) — a still-pending\nsubmission belongs to the moderation queue (`GET /contributions`,\nmoderator-only), not this public read view."},"ActivityFeedOut":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ActivityFeedItem"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["items","total","limit","offset"],"title":"ActivityFeedOut"},"AdminUserListOut":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AdminUserOut"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["items","total","limit","offset"],"title":"AdminUserListOut"},"AdminUserOut":{"properties":{"id":{"type":"integer","title":"Id"},"role":{"type":"string","title":"Role"},"github_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Github Id"},"discord_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Discord Id"},"google_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Google Id"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"approved_count":{"type":"integer","title":"Approved Count"},"rejected_count":{"type":"integer","title":"Rejected Count"},"trust_score":{"type":"integer","title":"Trust Score"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","role","github_id","discord_id","google_id","display_name","approved_count","rejected_count","trust_score","created_at"],"title":"AdminUserOut"},"AniListLookupOut":{"properties":{"status":{"type":"string","enum":["found","already_exists","not_found"],"title":"Status"},"anilist_id":{"type":"integer","title":"Anilist Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Format"},"episode_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Episode Count"},"cover_image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cover Image Url"},"existing_series_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Existing Series Id"},"existing_series_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Existing Series Slug"}},"type":"object","required":["status","anilist_id"],"title":"AniListLookupOut","description":"#165: response for GET /api/v1/anilist-lookup/{anilist_id} — the\nseries-proposal form's blur-triggered lookup on the anilist_id field.\n\nThree mutually exclusive outcomes via `status`, all returned as a\nnormal 200 (none of these is an error — \"not found\"/\"already exists\"\nare both legitimate, expected results for a live user-facing lookup):\n\n- \"already_exists\": the id already belongs to a live `series` row —\n  existing_series_id/slug/title are populated, format/episode_count/\n  cover_image_url are not (no AniList call was even made, see\n  services/anilist_lookup.py).\n- \"found\": a real, not-yet-catalogued AniList entry — title/format/\n  episode_count/cover_image_url are populated, existing_series_* are\n  not.\n- \"not_found\": no such AniList id, or AniList couldn't be reached —\n  every field beyond status/anilist_id is null."},"BulkApproveRequest":{"properties":{"ids":{"items":{"type":"integer"},"type":"array","maxItems":500,"minItems":1,"title":"Ids"}},"type":"object","required":["ids"],"title":"BulkApproveRequest"},"BulkContributionCreate":{"properties":{"canon_ranges":{"type":"string","title":"Canon Ranges","default":""},"mixed_ranges":{"type":"string","title":"Mixed Ranges","default":""},"filler_ranges":{"type":"string","title":"Filler Ranges","default":""},"citation":{"$ref":"#/components/schemas/CitationIn"},"license_accepted":{"type":"boolean","title":"License Accepted"},"dry_run":{"type":"boolean","title":"Dry Run","default":false}},"type":"object","required":["citation","license_accepted"],"title":"BulkContributionCreate","description":"#80: reuses the community's own existing range-notation shorthand\n(\"1-44, 48-49, 52-53\") rather than a new format contributors would\nhave to learn — the exact style already used to hand-compile every\ndataset in data/bootstrap/. Any of the three may be empty (a\nsubmission doesn't need all three categories), but at least one\nepisode must be declared across all three combined."},"BulkContributionResult":{"properties":{"dry_run":{"type":"boolean","title":"Dry Run"},"declared_count":{"type":"integer","title":"Declared Count"},"created":{"items":{"$ref":"#/components/schemas/BulkCreatedEntry"},"type":"array","title":"Created"},"skipped_conflicts":{"items":{"$ref":"#/components/schemas/BulkSkippedEntry"},"type":"array","title":"Skipped Conflicts"}},"type":"object","required":["dry_run","declared_count","created","skipped_conflicts"],"title":"BulkContributionResult"},"BulkCreatedEntry":{"properties":{"episode_number":{"type":"integer","title":"Episode Number"},"contribution_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contribution Id"},"proposed_status":{"type":"string","title":"Proposed Status"}},"type":"object","required":["episode_number","contribution_id","proposed_status"],"title":"BulkCreatedEntry"},"BulkModerationEntry":{"properties":{"id":{"type":"integer","title":"Id"},"ok":{"type":"boolean","title":"Ok"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["id","ok"],"title":"BulkModerationEntry"},"BulkModerationResult":{"properties":{"results":{"items":{"$ref":"#/components/schemas/BulkModerationEntry"},"type":"array","title":"Results"}},"type":"object","required":["results"],"title":"BulkModerationResult"},"BulkRejectRequest":{"properties":{"ids":{"items":{"type":"integer"},"type":"array","maxItems":500,"minItems":1,"title":"Ids"},"review_note":{"type":"string","minLength":1,"title":"Review Note"}},"type":"object","required":["ids","review_note"],"title":"BulkRejectRequest"},"BulkSkippedEntry":{"properties":{"episode_number":{"type":"integer","title":"Episode Number"},"existing_contribution_id":{"type":"integer","title":"Existing Contribution Id"}},"type":"object","required":["episode_number","existing_contribution_id"],"title":"BulkSkippedEntry","description":"#20's one-pending-per-episode rule, applied per episode within a\nbatch rather than failing the whole submission — the rest of the\nbatch still goes through; this just tells the submitter which numbers\ndidn't, and points at the existing contribution so they can endorse/\ndispute it directly instead."},"BulkSubmissionRateLimited":{"properties":{"detail":{"type":"string","title":"Detail"}},"type":"object","required":["detail"],"title":"BulkSubmissionRateLimited","description":"429 response body when #84's per-account rolling-window bulk-\nsubmission limit is exceeded. `detail` is a plain message (unlike\nDuplicatePendingContribution's structured detail) since there's no\nspecific resource to point the caller at — just \"wait\" or \"use\ndry_run.\""},"CitationIn":{"properties":{"url":{"anyOf":[{"type":"string","maxLength":3000},{"type":"null"}],"title":"Url"},"description":{"type":"string","maxLength":3000,"minLength":1,"title":"Description"},"methodology_note":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Methodology Note"},"source_count":{"type":"integer","minimum":1.0,"title":"Source Count","default":1}},"type":"object","required":["description"],"title":"CitationIn"},"CitationOut":{"properties":{"id":{"type":"integer","title":"Id"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"description":{"type":"string","title":"Description"},"source_count":{"type":"integer","title":"Source Count","default":1},"methodology_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Methodology Note"}},"type":"object","required":["id","url","description"],"title":"CitationOut"},"ContributionCreate":{"properties":{"series_id":{"type":"integer","title":"Series Id"},"episode_number":{"type":"integer","minimum":1.0,"title":"Episode Number"},"proposed_status":{"type":"string","pattern":"^(canon|filler|mixed)$","title":"Proposed Status"},"proposed_note":{"anyOf":[{"type":"string","maxLength":3000},{"type":"null"}],"title":"Proposed Note"},"citation":{"$ref":"#/components/schemas/CitationIn"},"license_accepted":{"type":"boolean","title":"License Accepted"},"turnstile_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turnstile Token"}},"type":"object","required":["series_id","episode_number","proposed_status","citation","license_accepted"],"title":"ContributionCreate"},"ContributionHistoryEntry":{"properties":{"id":{"type":"integer","title":"Id"},"proposed_status":{"type":"string","title":"Proposed Status"},"proposed_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposed Note"},"citation":{"$ref":"#/components/schemas/CitationOut"},"submitted_by":{"anyOf":[{"$ref":"#/components/schemas/UserRef"},{"type":"null"}]},"submitted_at":{"type":"string","format":"date-time","title":"Submitted At"},"review_status":{"type":"string","title":"Review Status"},"resolution_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Method"},"reviewed_by":{"anyOf":[{"$ref":"#/components/schemas/UserRef"},{"type":"null"}]},"reviewed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reviewed At"},"review_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Note"},"votes":{"items":{"$ref":"#/components/schemas/VoteOut"},"type":"array","title":"Votes"}},"type":"object","required":["id","proposed_status","proposed_note","citation","submitted_by","submitted_at","review_status","resolution_method","reviewed_by","reviewed_at","review_note","votes"],"title":"ContributionHistoryEntry"},"ContributionOut":{"properties":{"id":{"type":"integer","title":"Id"},"series_id":{"type":"integer","title":"Series Id"},"episode_number":{"type":"integer","title":"Episode Number"},"proposed_status":{"type":"string","title":"Proposed Status"},"proposed_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposed Note"},"citation":{"$ref":"#/components/schemas/CitationOut"},"submitted_at":{"type":"string","format":"date-time","title":"Submitted At"},"review_status":{"type":"string","title":"Review Status"},"resolution_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Method"},"reviewed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reviewed At"},"review_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Note"}},"type":"object","required":["id","series_id","episode_number","proposed_status","proposed_note","citation","submitted_at","review_status","resolution_method","reviewed_at","review_note"],"title":"ContributionOut"},"ContributionReject":{"properties":{"review_note":{"type":"string","minLength":1,"title":"Review Note"}},"type":"object","required":["review_note"],"title":"ContributionReject"},"ContributionReviewOut":{"properties":{"id":{"type":"integer","title":"Id"},"review_status":{"type":"string","title":"Review Status"},"resolution_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Method"},"reviewed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reviewed At"},"review_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Note"}},"type":"object","required":["id","review_status","resolution_method","reviewed_at","review_note"],"title":"ContributionReviewOut"},"DuplicatePendingContribution":{"properties":{"detail":{"$ref":"#/components/schemas/DuplicatePendingContributionDetail"}},"type":"object","required":["detail"],"title":"DuplicatePendingContribution","description":"409 response body when #20's one-pending-per-episode rule rejects a\nsubmission — points the caller at the existing pending contribution so\na client can offer \"endorse/dispute this instead\" rather than a dead\nend. Nested under `detail` to match what FastAPI's HTTPException\nactually produces, not a guessed flat shape."},"DuplicatePendingContributionDetail":{"properties":{"message":{"type":"string","title":"Message"},"existing_contribution_id":{"type":"integer","title":"Existing Contribution Id"}},"type":"object","required":["message","existing_contribution_id"],"title":"DuplicatePendingContributionDetail"},"DuplicatePendingSynonymSuggestion":{"properties":{"detail":{"$ref":"#/components/schemas/DuplicatePendingSynonymSuggestionDetail"}},"type":"object","required":["detail"],"title":"DuplicatePendingSynonymSuggestion","description":"409 response body when the one-pending-per-(series_id, synonym)\nrule (migrations/015) rejects a submission — points the caller at the\nexisting pending suggestion, same precedent as contributions'\nDuplicatePendingContribution."},"DuplicatePendingSynonymSuggestionDetail":{"properties":{"message":{"type":"string","title":"Message"},"existing_suggestion_id":{"type":"integer","title":"Existing Suggestion Id"}},"type":"object","required":["message","existing_suggestion_id"],"title":"DuplicatePendingSynonymSuggestionDetail"},"EpisodeDataIn":{"properties":{"canon_ranges":{"type":"string","title":"Canon Ranges","default":""},"mixed_ranges":{"type":"string","title":"Mixed Ranges","default":""},"filler_ranges":{"type":"string","title":"Filler Ranges","default":""},"citation":{"$ref":"#/components/schemas/CitationIn"}},"type":"object","required":["citation"],"title":"EpisodeDataIn","description":"#85: a contributor's optional episode-range data, attached to a\nseries proposal in the same shape #80's bulk-submission endpoint\nalready expects — held on the proposal row (see services/\nseries_proposals.py) until approval, when it's turned into real\ncitations + contributions targeting the newly-created series. No\nlicense_accepted/dry_run fields here, unlike BulkContributionCreate —\nthe proposal's own top-level license_accepted covers this same\nsubmission event, and there's no series_id yet to preview a dry run\nagainst."},"EpisodeDataOut":{"properties":{"canon_ranges":{"type":"string","title":"Canon Ranges","default":""},"mixed_ranges":{"type":"string","title":"Mixed Ranges","default":""},"filler_ranges":{"type":"string","title":"Filler Ranges","default":""},"citation":{"$ref":"#/components/schemas/CitationIn"},"declared_count":{"type":"integer","title":"Declared Count"}},"type":"object","required":["citation","declared_count"],"title":"EpisodeDataOut"},"EpisodeOut":{"properties":{"id":{"type":"integer","title":"Id"},"series_id":{"type":"integer","title":"Series Id"},"episode_number":{"type":"integer","title":"Episode Number"},"status":{"type":"string","title":"Status"},"status_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Note"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"citation":{"$ref":"#/components/schemas/CitationOut"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"aired_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Aired At"},"has_pending_contribution":{"type":"boolean","title":"Has Pending Contribution"}},"type":"object","required":["id","series_id","episode_number","status","status_note","title","citation","updated_at","aired_at","has_pending_contribution"],"title":"EpisodeOut"},"ErrorDetail":{"properties":{"detail":{"type":"string","title":"Detail"}},"type":"object","required":["detail"],"title":"ErrorDetail"},"ExportAccessRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"license_accepted":{"type":"boolean","title":"License Accepted"}},"type":"object","required":["email","license_accepted"],"title":"ExportAccessRequest"},"ExportAccessResponse":{"properties":{"api_key":{"type":"string","title":"Api Key"},"note":{"type":"string","title":"Note","default":"This key is shown once and cannot be retrieved again. Store it securely; request a new one via /export/request-access if lost."}},"type":"object","required":["api_key"],"title":"ExportAccessResponse"},"ExportEpisodeOut":{"properties":{"episode_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Episode Id"},"episode_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Episode Number"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"status_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Note"},"citation_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Citation Url"},"citation_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Citation Description"}},"type":"object","required":["episode_id","episode_number","status","status_note","citation_url","citation_description"],"title":"ExportEpisodeOut"},"ExportManifest":{"properties":{"license":{"type":"string","title":"License","default":"CC BY-NC-SA 4.0"},"attribution_notice":{"type":"string","title":"Attribution Notice","default":"Contains information from AniFillerPedia, which is made available here under CC BY-NC-SA 4.0 (non-commercial use; contact us for a commercial license)."},"commercial_licensing_contact":{"type":"string","title":"Commercial Licensing Contact","default":"See https://github.com/Napandee/AniFillerPedia DATA_LICENSE for the current commercial-licensing contact channel."},"dataset_license_url":{"type":"string","title":"Dataset License Url","default":"https://github.com/Napandee/AniFillerPedia/blob/master/DATA_LICENSE"}},"type":"object","title":"ExportManifest"},"ExportOut":{"properties":{"manifest":{"$ref":"#/components/schemas/ExportManifest"},"series":{"items":{"$ref":"#/components/schemas/ExportSeriesOut"},"type":"array","title":"Series"}},"type":"object","required":["manifest","series"],"title":"ExportOut"},"ExportSeriesOut":{"properties":{"series_id":{"type":"integer","title":"Series Id"},"anilist_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anilist Id"},"mal_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Mal Id"},"anidb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anidb Id"},"title":{"type":"string","title":"Title"},"provenance":{"type":"string","title":"Provenance"},"episodes":{"items":{"$ref":"#/components/schemas/ExportEpisodeOut"},"type":"array","title":"Episodes"}},"type":"object","required":["series_id","anilist_id","mal_id","anidb_id","title","provenance","episodes"],"title":"ExportSeriesOut"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LocalLoginIn":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","maxLength":200,"minLength":1,"title":"Password"}},"type":"object","required":["email","password"],"title":"LocalLoginIn"},"LocalSignupIn":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","maxLength":200,"minLength":8,"title":"Password"},"display_name":{"type":"string","maxLength":100,"minLength":1,"title":"Display Name"}},"type":"object","required":["email","password","display_name"],"title":"LocalSignupIn"},"MyVoteOut":{"properties":{"contribution_id":{"type":"integer","title":"Contribution Id"},"series_id":{"type":"integer","title":"Series Id"},"series_title":{"type":"string","title":"Series Title"},"episode_number":{"type":"integer","title":"Episode Number"},"proposed_status":{"type":"string","title":"Proposed Status"},"vote":{"type":"string","title":"Vote"},"weight_at_vote":{"type":"integer","title":"Weight At Vote"},"review_status":{"type":"string","title":"Review Status"},"resolution_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Method"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["contribution_id","series_id","series_title","episode_number","proposed_status","vote","weight_at_vote","review_status","resolution_method","created_at"],"title":"MyVoteOut","description":"#30: one entry per vote the caller has cast, enough context\n(series title, episode, current resolution) to render without a\nfollow-up request per row."},"NeedsResearchItem":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"type":"string","title":"Title"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"reason":{"type":"string","title":"Reason"},"anilist_episode_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anilist Episode Count"},"airing_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Airing Status"},"researched_episode_count":{"type":"integer","title":"Researched Episode Count"}},"type":"object","required":["id","title","slug","reason","anilist_episode_count","airing_status","researched_episode_count"],"title":"NeedsResearchItem","description":"#153: one row of the public \"needs research\" queue — a series with\nzero episode rows (`never_researched`), or a series #175's weekly\ndrift worker has flagged as no longer matching AniList's live state\n(`status_drift` / `episode_count_drift`, read straight from\n`series.anilist_drift_reason` rather than re-derived here). The\nissue's own scope note guarantees these two cases never overlap: a\ndrift-flagged series always has episodes to have drifted from, so a\nrow is always exactly one reason, never both."},"NeedsResearchListOut":{"properties":{"items":{"items":{"$ref":"#/components/schemas/NeedsResearchItem"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["items","total","limit","offset"],"title":"NeedsResearchListOut"},"RoleUpdateIn":{"properties":{"role":{"type":"string","title":"Role","description":"One of: contributor, moderator, admin. 'owner' is deliberately not a valid value here — it is set once at bootstrap and is never assignable through this endpoint. Setting 'admin' requires the caller to be the owner."}},"type":"object","required":["role"],"title":"RoleUpdateIn"},"RoleUpdateOut":{"properties":{"id":{"type":"integer","title":"Id"},"role":{"type":"string","title":"Role"}},"type":"object","required":["id","role"],"title":"RoleUpdateOut","description":"#138: PATCH /admin/users/{id}/role previously had no response_model\nat all (returned a bare dict) — this documents the real shape."},"SeriesDetailOut":{"properties":{"id":{"type":"integer","title":"Id"},"anilist_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anilist Id"},"mal_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Mal Id"},"anidb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anidb Id"},"title":{"type":"string","title":"Title"},"provenance":{"type":"string","title":"Provenance"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"cover_image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cover Image Url"},"banner_image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Image Url"},"airing_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Airing Status"},"sequence_order":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sequence Order"},"synonyms":{"items":{"type":"string"},"type":"array","title":"Synonyms"},"anilist_episode_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anilist Episode Count"},"related_series":{"items":{"$ref":"#/components/schemas/SeriesOut"},"type":"array","title":"Related Series"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"start_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Start Date"},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date"},"next_series":{"anyOf":[{"$ref":"#/components/schemas/SeriesOut"},{"type":"null"}]},"previous_series":{"anyOf":[{"$ref":"#/components/schemas/SeriesOut"},{"type":"null"}]}},"type":"object","required":["id","anilist_id","mal_id","anidb_id","title","provenance","created_at","slug","cover_image_url","banner_image_url","airing_status","synonyms","anilist_episode_count","related_series","description","start_date","end_date","next_series","previous_series"],"title":"SeriesDetailOut"},"SeriesListOut":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SeriesOut"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["items","total","limit","offset"],"title":"SeriesListOut"},"SeriesOut":{"properties":{"id":{"type":"integer","title":"Id"},"anilist_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anilist Id"},"mal_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Mal Id"},"anidb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anidb Id"},"title":{"type":"string","title":"Title"},"provenance":{"type":"string","title":"Provenance"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"cover_image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cover Image Url"},"banner_image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Image Url"},"airing_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Airing Status"},"sequence_order":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sequence Order"}},"type":"object","required":["id","anilist_id","mal_id","anidb_id","title","provenance","created_at","slug","cover_image_url","banner_image_url","airing_status"],"title":"SeriesOut"},"SeriesProposalCreate":{"properties":{"title":{"type":"string","minLength":1,"title":"Title"},"anilist_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anilist Id"},"mal_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Mal Id"},"anidb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anidb Id"},"justification":{"type":"string","maxLength":3000,"minLength":1,"title":"Justification"},"license_accepted":{"type":"boolean","title":"License Accepted"},"turnstile_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turnstile Token"},"episode_data":{"anyOf":[{"$ref":"#/components/schemas/EpisodeDataIn"},{"type":"null"}]}},"type":"object","required":["title","justification","license_accepted"],"title":"SeriesProposalCreate"},"SeriesProposalOut":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"type":"string","title":"Title"},"anilist_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anilist Id"},"mal_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Mal Id"},"anidb_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anidb Id"},"justification":{"type":"string","title":"Justification"},"submitted_at":{"type":"string","format":"date-time","title":"Submitted At"},"review_status":{"type":"string","title":"Review Status"},"reviewed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reviewed At"},"review_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Note"},"episode_data":{"anyOf":[{"$ref":"#/components/schemas/EpisodeDataOut"},{"type":"null"}]},"possible_duplicate_matches":{"items":{"$ref":"#/components/schemas/SimilarSeriesMatchOut"},"type":"array","title":"Possible Duplicate Matches"}},"type":"object","required":["id","title","anilist_id","mal_id","anidb_id","justification","submitted_at","review_status","reviewed_at","review_note"],"title":"SeriesProposalOut"},"SeriesProposalReject":{"properties":{"review_note":{"type":"string","minLength":1,"title":"Review Note"}},"type":"object","required":["review_note"],"title":"SeriesProposalReject"},"SeriesProposalReviewOut":{"properties":{"id":{"type":"integer","title":"Id"},"review_status":{"type":"string","title":"Review Status"},"reviewed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reviewed At"},"review_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Note"},"episode_contributions_created":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Episode Contributions Created"}},"type":"object","required":["id","review_status","reviewed_at","review_note"],"title":"SeriesProposalReviewOut"},"SimilarSeriesCheckOut":{"properties":{"matches":{"items":{"$ref":"#/components/schemas/SimilarSeriesMatchOut"},"type":"array","title":"Matches"}},"type":"object","required":["matches"],"title":"SimilarSeriesCheckOut","description":"#150: response shape for the standalone, pre-submission\nGET /series-proposals/check-title lookup the frontend calls on\nTitle-field blur — same matches list as SeriesProposalOut's own\npossible_duplicate_matches, just available before a submitter has\ncommitted to the rest of the form."},"SimilarSeriesMatchOut":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"type":"string","title":"Title"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"}},"type":"object","required":["id","title"],"title":"SimilarSeriesMatchOut","description":"#150: a single \"this might already exist\" hint against the live\n`series` catalog. Surfaced both in the submission response (frontend\nshows it to the submitter right after they submit) and in the\nmoderation-queue listing (services/series_proposals.py's _row_to_out\ncomputes this fresh every time a proposal is serialized, so a\nmoderator always sees it against the CURRENT catalog, not a stale\nsnapshot from submission time) — never blocking, just a pointer."},"SuspensionUpdateIn":{"properties":{"suspended":{"type":"boolean","title":"Suspended"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason","description":"Moderator-facing note explaining the suspension. Ignored (cleared) when suspended=false."}},"type":"object","required":["suspended"],"title":"SuspensionUpdateIn","description":"#209: body for PATCH /admin/users/{id}/suspension."},"SuspensionUpdateOut":{"properties":{"id":{"type":"integer","title":"Id"},"suspended":{"type":"boolean","title":"Suspended"},"suspended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Suspended At"},"suspended_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Suspended Reason"}},"type":"object","required":["id","suspended","suspended_at","suspended_reason"],"title":"SuspensionUpdateOut"},"SynonymSuggestionCreate":{"properties":{"series_id":{"type":"integer","title":"Series Id"},"synonym":{"type":"string","maxLength":200,"minLength":1,"title":"Synonym"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Note"},"license_accepted":{"type":"boolean","title":"License Accepted"},"turnstile_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turnstile Token"}},"type":"object","required":["series_id","synonym","license_accepted"],"title":"SynonymSuggestionCreate"},"SynonymSuggestionOut":{"properties":{"id":{"type":"integer","title":"Id"},"series_id":{"type":"integer","title":"Series Id"},"series_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Series Title"},"series_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Series Slug"},"synonym":{"type":"string","title":"Synonym"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"submitted_at":{"type":"string","format":"date-time","title":"Submitted At"},"review_status":{"type":"string","title":"Review Status"},"reviewed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reviewed At"},"review_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Note"}},"type":"object","required":["id","series_id","synonym","note","submitted_at","review_status","reviewed_at","review_note"],"title":"SynonymSuggestionOut"},"SynonymSuggestionReject":{"properties":{"review_note":{"type":"string","minLength":1,"title":"Review Note"}},"type":"object","required":["review_note"],"title":"SynonymSuggestionReject"},"SynonymSuggestionReviewOut":{"properties":{"id":{"type":"integer","title":"Id"},"review_status":{"type":"string","title":"Review Status"},"reviewed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reviewed At"},"review_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Note"}},"type":"object","required":["id","review_status","reviewed_at","review_note"],"title":"SynonymSuggestionReviewOut"},"TrafficCountryEntryOut":{"properties":{"country":{"type":"string","title":"Country"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["country","count"],"title":"TrafficCountryEntryOut"},"TrafficPathEntryOut":{"properties":{"path":{"type":"string","title":"Path"},"path_kind":{"type":"string","title":"Path Kind","description":"'frontend' or 'api' — split on whether path starts with /api/v1/"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["path","path_kind","count"],"title":"TrafficPathEntryOut","description":"One entry in a day's top_paths — see services/traffic_analytics.py's\naggregate_rollup() for how this is computed."},"TrafficRollupListOut":{"properties":{"items":{"items":{"$ref":"#/components/schemas/TrafficRollupOut"},"type":"array","title":"Items"}},"type":"object","required":["items"],"title":"TrafficRollupListOut"},"TrafficRollupOut":{"properties":{"rollup_date":{"type":"string","title":"Rollup Date"},"total_requests":{"type":"integer","title":"Total Requests"},"top_paths":{"items":{"$ref":"#/components/schemas/TrafficPathEntryOut"},"type":"array","title":"Top Paths"},"status_breakdown":{"items":{"$ref":"#/components/schemas/TrafficStatusEntryOut"},"type":"array","title":"Status Breakdown"},"top_countries":{"items":{"$ref":"#/components/schemas/TrafficCountryEntryOut"},"type":"array","title":"Top Countries"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["rollup_date","total_requests","top_paths","status_breakdown","top_countries","created_at"],"title":"TrafficRollupOut","description":"#221: one day's persisted Cloudflare traffic rollup."},"TrafficStatusEntryOut":{"properties":{"status":{"type":"integer","title":"Status"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["status","count"],"title":"TrafficStatusEntryOut"},"UserExportOut":{"properties":{"profile":{"$ref":"#/components/schemas/UserOut"},"contributions":{"items":{"$ref":"#/components/schemas/ContributionOut"},"type":"array","title":"Contributions"},"votes":{"items":{"$ref":"#/components/schemas/MyVoteOut"},"type":"array","title":"Votes"},"series_proposals":{"items":{"$ref":"#/components/schemas/SeriesProposalOut"},"type":"array","title":"Series Proposals"},"synonym_suggestions":{"items":{"$ref":"#/components/schemas/SynonymSuggestionOut"},"type":"array","title":"Synonym Suggestions"}},"type":"object","required":["profile","contributions","votes","series_proposals","synonym_suggestions"],"title":"UserExportOut","description":"#208: GDPR Article 15 bundle — the caller's full profile plus\neverything they've submitted/voted on, in one response. Reuses the\nexact same repository/service queries each `/mine`-scoped endpoint\nalready exposes separately (services/contributions.py's\nlist_my_contributions/list_my_votes, services/series_proposals.py's\nlist_my_series_proposals, services/synonym_suggestions.py's\nlist_my_synonym_suggestions) rather than reimplementing any of them —\nsee routers/users.py's export_current_user_data."},"UserOut":{"properties":{"id":{"type":"integer","title":"Id"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url"},"role":{"type":"string","title":"Role"},"approved_count":{"type":"integer","title":"Approved Count"},"rejected_count":{"type":"integer","title":"Rejected Count"},"trust_score":{"type":"integer","title":"Trust Score"}},"type":"object","required":["id","email","display_name","avatar_url","role","approved_count","rejected_count","trust_score"],"title":"UserOut"},"UserRef":{"properties":{"id":{"type":"integer","title":"Id"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"github_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Github Id"}},"type":"object","required":["id","display_name","github_id"],"title":"UserRef","description":"Public identity — usernames visible per CLAUDE.md's decision to keep\ncontribution history non-anonymized. Absent (None) covers both a\ngenuinely anonymous submission and an anonymized-by-deletion account —\nthose look identical on purpose (schema.sql: ON DELETE SET NULL exists\nspecifically so deletion can't be told apart from anonymity after the\nfact)."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VoteCastOut":{"properties":{"contribution_id":{"type":"integer","title":"Contribution Id"},"vote":{"type":"string","title":"Vote"},"weight_at_vote":{"type":"integer","title":"Weight At Vote"},"net_score":{"type":"integer","title":"Net Score"},"auto_approval_threshold":{"type":"integer","title":"Auto Approval Threshold"},"review_status":{"type":"string","title":"Review Status"},"resolution_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Method"}},"type":"object","required":["contribution_id","vote","weight_at_vote","net_score","auto_approval_threshold","review_status","resolution_method"],"title":"VoteCastOut","description":"Response for POST /contributions/{id}/vote — reports the outcome of\nTHIS vote (weight_at_vote, the vote's own contribution to net_score) as\nwell as the contribution's resulting state, which may reflect a\ndifferent concurrent voter's promotion rather than this one (see\nrepositories/contributions.py's promote_via_vote race note) — always\nre-read from the row after voting rather than assumed from this\nrequest alone."},"VoteClusteringPairOut":{"properties":{"user_a_id":{"type":"integer","title":"User A Id"},"user_a_display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User A Display Name"},"user_b_id":{"type":"integer","title":"User B Id"},"user_b_display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User B Display Name"},"a_endorsed_b_count":{"type":"integer","title":"A Endorsed B Count"},"b_endorsed_a_count":{"type":"integer","title":"B Endorsed A Count"},"combined_endorsement_count":{"type":"integer","title":"Combined Endorsement Count"},"last_activity_at":{"type":"string","title":"Last Activity At"}},"type":"object","required":["user_a_id","user_a_display_name","user_b_id","user_b_display_name","a_endorsed_b_count","b_endorsed_a_count","combined_endorsement_count","last_activity_at"],"title":"VoteClusteringPairOut","description":"#203: one reciprocal-endorsement pair surfaced by the Sybil-\nmonitoring report — see services/admin.py's get_vote_clustering_report."},"VoteClusteringReportOut":{"properties":{"items":{"items":{"$ref":"#/components/schemas/VoteClusteringPairOut"},"type":"array","title":"Items"},"min_reciprocal_count":{"type":"integer","title":"Min Reciprocal Count"}},"type":"object","required":["items","min_reciprocal_count"],"title":"VoteClusteringReportOut"},"VoteCreate":{"properties":{"vote":{"type":"string","pattern":"^(endorse|dispute)$","title":"Vote"}},"type":"object","required":["vote"],"title":"VoteCreate"},"VoteOut":{"properties":{"voter":{"anyOf":[{"$ref":"#/components/schemas/UserRef"},{"type":"null"}]},"vote":{"type":"string","title":"Vote"},"weight_at_vote":{"type":"integer","title":"Weight At Vote"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["voter","vote","weight_at_vote","created_at"],"title":"VoteOut"}}},"tags":[{"name":"series","description":"Public, unauthenticated reads of the series catalog — search, detail (by id or slug), per-series episode lists, and (#153) the \"needs research\" queue of series with zero episode data or AniList drift. See docs/API.md."},{"name":"episodes","description":"Public, unauthenticated reads of a single episode and its full contribution/review history."},{"name":"contributions","description":"Submit and review per-episode filler/canon/mixed corrections — single-episode and bulk-range submission, moderator approve/reject, and community trust-weighted voting. See CONTRIBUTING.md."},{"name":"series-proposals","description":"Propose a series that isn't in the catalog yet (optionally with attached bulk episode data), and the moderator approve/reject flow for it. Mirrors the contributions workflow above."},{"name":"synonym-suggestions","description":"Suggest an alternate/dub/regional title for an already-catalogued series, and the moderator approve/reject flow for it (#148). Moderator-only approval, not community voting — see services/synonym_suggestions.py for why."},{"name":"admin","description":"Admin/owner-only user and role management. Not moderation — see the contributions/series-proposals routers for the approval queue."},{"name":"auth","description":"Cookie-based OAuth login (GitHub, Discord) — the redirect-based flow a browser drives. See docs/API.md's Authentication section for what a non-browser (server-to-server) caller can and can't do here."},{"name":"settings","description":"Authenticated account settings — currently just linking an additional OAuth provider to an already-signed-in account."},{"name":"users","description":"The signed-in caller's own account — profile/trust-score read, self-service GDPR deletion."},{"name":"export","description":"The bulk dataset dump — the one part of this API that isn't fully open, since a silent anonymous bulk download has weaker license-agreement standing than a click-through API-key request does."},{"name":"legal","description":"Static/structured legal pages — privacy policy and the dataset's CC BY-NC-SA license + attribution/commercial-contact details."},{"name":"anilist-lookup","description":"A thin, rate-limited proxy to AniList's own public GraphQL API, used by submission forms to pre-fill a title from an AniList id."},{"name":"activity","description":"Public, read-only \"recent changes\" feed (#154, JSON and RSS) over the existing contributions/series-proposals audit trail — every resolved (approved/rejected/withdrawn) submission, newest first."},{"name":"health","description":"Liveness/version check — no auth, no dependencies."}]}