Authentication
Endpoints marked with
Auth require the header:
Authorization: Bearer <access_token>Access tokens are JWT with a
15-minute lifetime. Refresh tokens last
30 days . When the access token expires, use
POST /api/v1/auth/refresh to obtain a new pair.
ALL GET POST PATCH DELETE
All Authenticated Public
371 endpoints Health 2 endpoints Service health and readiness probes.
GET /health Basic health check. Returns 200 if the service is running. GET /ready Readiness check. Verifies database and Redis connections. Authentication 24 endpoints Register, login, token management, and OAuth flows. Access tokens are JWT with a 15-minute lifetime. Refresh tokens last 30 days.
POST /api/v1/auth/2fa/backup-codes Regenerate backup codes. Previous codes are invalidated. AuthDELETE /api/v1/auth/2fa/disable Disable 2FA on the authenticated account. Requires the current password for confirmation. AuthPOST /api/v1/auth/2fa/enable Enable 2FA after verifying a TOTP code generated from the setup secret. Returns one-time backup codes. AuthPOST /api/v1/auth/2fa/setup Generate a new TOTP secret and QR URI. Returns the secret (temporarily stored server-side) and the provisioning URI to display as a QR code. AuthPOST /api/v1/auth/2fa/verify-login Complete a 2FA login challenge. Called when /login returns `requires_2fa:true` with a short-lived challenge token. POST /api/v1/auth/forgot-password Initiate password reset. Sends an email with a reset link. Always returns 200 (anti-enumeration). Rate-limited to 2 per 5 minutes per IP. POST /api/v1/auth/login Authenticate with email-or-username and password. Identifier with @ is treated as email; otherwise looked up as username. Lockout: 10 failed attempts in 15 min triggers a temporary lock — applied per IP AND per resolved user account so a distributed botnet can't hammer one account from many IPs. POST /api/v1/auth/logout Invalidate the current session and refresh token. AuthGET /api/v1/auth/me Get the authenticated user's full profile. AuthGET /api/v1/auth/oauth/:provider Initiate OAuth flow. Redirects user to provider (shikimori, google, yandex, discord). Optional `?mobile_redirect=anilay://auth/callback` stashes a deep-link target so the callback can send tokens back to the mobile app instead of the web frontend (scheme must be anilay://). GET /api/v1/auth/oauth/:provider/callback OAuth callback handler. Exchanges code for tokens and creates/links account. Redirects to the web frontend by default, or to the mobile deep link if mobile_redirect was set on init — in which case `access_token` and `refresh_token` are passed as query params. POST /api/v1/auth/oauth/:provider/link Link an OAuth provider to the authenticated account. AuthDELETE /api/v1/auth/oauth/:provider/link Unlink an OAuth provider from the authenticated account. AuthGET /api/v1/auth/oauth/providers List all OAuth providers linked to the authenticated account. AuthPOST /api/v1/auth/password Set or change the authenticated user's password. If the account already has a password (current_password proves ownership), supply current_password. For OAuth-only accounts setting their first password, supply confirm_email matching the account's email. On success, revokes every other refresh token for the user — keeps the caller's current session, logs out every other device. AuthPOST /api/v1/auth/refresh Exchange a valid refresh token for a new token pair. Web clients send the RT in the HttpOnly cookie. Mobile clients send `Authorization: Bearer <refresh_token>` — in that case the response body includes `refresh_token` and no cookie is set. POST /api/v1/auth/register Register a new user account. POST /api/v1/auth/request-verification Send a verification email to the given address. Rate-limited to 2/hr per IP. POST /api/v1/auth/resend-verification Resend the verification email to the authenticated user. Rate-limited to 2 per 5 minutes per user. AuthPOST /api/v1/auth/reset-password Complete password reset with a token. Revokes all existing sessions on success. Rate-limited to 5/min per IP. GET /api/v1/auth/sessions List all active sessions for the authenticated user, with device/IP info. AuthDELETE /api/v1/auth/sessions Revoke all sessions except the current one. AuthDELETE /api/v1/auth/sessions/:id Revoke a specific session by ID. If it matches the current token, the caller is logged out. AuthPOST /api/v1/auth/verify-email Verify an email address using the code sent via email. Rate-limited to 5/min per IP. Users 14 endpoints Public user profiles and search.
GET /api/v1/users/:username Get a user's public profile by username. GET /api/v1/users/batch Resolve a batch of user UUIDs to public profiles in one round-trip. Used by the chat client to hydrate message senders + derive online dots. Capped at 200 ids per call; invalid / unknown ids are silently dropped from the response. last_seen_at is included only when the target user's online_status_visibility is "everyone" (or when the caller is asking about themselves). PATCH /api/v1/users/me Update the authenticated user's profile. All fields optional. AuthPOST /api/v1/users/me/complete-setup Complete the first-login onboarding flow (username pick, initial customization). Called once after registration/OAuth link. AuthGET /api/v1/users/me/deletion Get the status of any pending account deletion request for the authenticated user. AuthDELETE /api/v1/users/me/deletion Cancel a pending deletion request during the grace window. AuthPOST /api/v1/users/me/deletion/confirm Step 2 of account deletion. Confirms with the email code. Account is soft-deleted and enters a 7-day grace window. AuthPOST /api/v1/users/me/deletion/request Step 1 of account deletion. Sends a confirmation code to the user's email. AuthGET /api/v1/users/me/export Export all personal data as a JSON download (GDPR-style). Rate-limited to 3 downloads per hour per user. AuthGET /api/v1/users/me/progress Per-user resume progress — every (anime, episode) row the user has touched. With ?anime_id=… returns every episode for one anime ordered by episode_num. Without it, returns the most-recently-touched rows for the "Continue watching" widget (limited via ?limit=, default 12, capped at 100). Anonymous users keep using localStorage; on sign-in the client posts /import to push localStorage rows up. AuthPUT /api/v1/users/me/progress Heartbeat: upsert one (anime, episode) resume row. Called every ~10s while playing, and on pause / seek / episode change / page unload. updated_at is always set server-side to now() — a client-supplied updated_at is honored only on /import. AuthDELETE /api/v1/users/me/progress Clear progress. With ?anime_id=… clears one anime's rows (matches "Remove from continue watching"). Without it, wipes every progress row for the user — the privacy / settings action. AuthPOST /api/v1/users/me/progress/import Bulk merge of pre-login localStorage rows into the user's account. Newer-wins per (anime, episode): overwrites a stored row only when the imported row's updated_at is strictly newer than what's in the DB (so progress earned on another device isn't clobbered). Capped at 5000 entries per call; bad rows (negative ep/timecode) are silently dropped from the batch rather than failing it. AuthGET /api/v1/users/search Search users by username or display name. Anime 12 endpoints Browse, search, and get detailed anime information. All endpoints are public and responses are cached.
GET /api/v1/anime List anime with pagination and filters. GET /api/v1/anime/:id Get full anime details by ID, including streaming info. Returns 410 Gone for non-admins when the row is moderated-hidden (admins/testers/superadmins bypass). Response includes `visibility` (public | player_restricted | hidden), `moderation_reason`, and external rating fields populated by the ratings worker: `score_mal`, `score_mal_scored_by`, `score_anilist`, `score_anilist_scored_by`, `score_kitsu`, `score_kitsu_scored_by`, `anilist_id`, `kitsu_id`, `external_scores_synced_at`. GET /api/v1/anime/:id/community-rating Aggregate of platform users' scores for a single anime (avg + rater count). Rows without a score are excluded. 404 if the anime is unknown locally. GET /api/v1/anime/:id/episodes Get episode list with streaming data for an anime. Returns 410 Gone for hidden rows (non-admin) and 403 `{ "error": "player restricted", "moderation_reason": "…" }` for player-restricted rows (non-admin). GET /api/v1/anime/:id/related Get related anime (sequels, prequels, side stories — full franchise). GET /api/v1/anime/:id/similar Get a list of similar anime. GET /api/v1/anime/:id/timecodes List admin-curated opening/ending timecodes for every reviewed (episode, dub) tuple of this anime. The player uses this exclusively to decide whether to render the Skip-Opening / Skip-Ending buttons — episodes without a row do not show the button. NULL intro/outro pairs encode "no opening (or ending) for this episode". Each row is keyed by (episode_number, translation_id); translation_id="" is the default fallback applied to any dub without its own override. The player picks the most-specific match for the active translation, falling back to "". GET /api/v1/anime/by-shikimori/:sid Get full anime details by Shikimori ID. Same payload shape as GET /anime/:id — used by the admin edit deep-link from the public /anime/:id page, which keys by shikimori_id. Returns 410 Gone for non-admins on moderated-hidden rows. GET /api/v1/anime/by-shikimori/:sid/community-rating Same as /anime/:id/community-rating but keyed by Shikimori ID. Returns { "average": null, "count": 0 } (200 OK) when the anime is not yet in our DB, so the public /anime/:sid page does not have to branch on 404. GET /api/v1/anime/by-shikimori/:sid/timecodes Same payload as /anime/:id/timecodes but keyed by Shikimori ID — the public anime page (which uses Shikimori IDs in its URL) calls this directly. Returns an empty list if the anime is not yet in our DB.
GET /api/v1/anime/calendar Get the weekly anime release calendar. GET /api/v1/anime/search Full-text search for anime titles. Anime List 8 endpoints Manage personal anime watch lists. Track status, score, and episode progress.
GET /api/v1/anime-list Get the authenticated user's own anime list. AuthPOST /api/v1/anime-list Add/upsert an anime in the authenticated user's list. Either anime_id or shikimori_id must be provided. If the anime row does not exist locally but shikimori_id is given, a stub is auto-created (the refresh worker then hydrates it). Status is optional — when omitted on a new entry it defaults to "planned" so score-only writes work without forcing the caller to pick a status. AuthPATCH /api/v1/anime-list/:anime_id Update an anime list entry. Status is optional; when omitted, the existing status is preserved (or defaults to "planned" if no entry exists yet). AuthDELETE /api/v1/anime-list/:anime_id Remove an anime from the list. AuthPOST /api/v1/anime-list/:anime_id/episode Mark the next episode as watched (increments episode count by 1). Also accrues progression age for the "Path of Ascension" system when the anti-abuse cooldown (15 min between episodes) is satisfied. AuthDELETE /api/v1/anime-list/shikimori/:shikimori_id Remove a list entry identified by its Shikimori ID (used when the internal anime record was never created). AuthGET /api/v1/users/:username/anime-list Get a user's anime list (public). Each entry includes a per-anime `progress` summary rolled up from user_anime_progress (mig 065) — null when the user has never opened the player for that anime. Powers the profile cards' "last on ep N · X:XX / Y minutes watched" without an N+1 fan-out. anime.shikimori_id on the entry is the key into the matching summary. GET /api/v1/users/:username/progress Batch per-anime resume progress for a user. Per-episode rows for every listed shikimori_id, grouped by anime_id. The list endpoint above already attaches a per-anime *summary*; this endpoint is for views that need the expanded episode-by-episode detail (e.g. an "episode bar" on each profile card). Anime not in the result map either have no progress rows or weren't in the input. Capped at 200 anime_ids per call. Collections 29 endpoints User-curated lists of anime. Three-tier visibility: public (indexed on the browse page), unlisted (link-only), private (owner-only). Items store the Shikimori ID plus denormalised title/poster so adding an anime does not require it to exist in our local catalog. Phase 1 covers CRUD + items; groups, social (likes/favorites/views/forks), comments, cover upload, and challenge mode arrive in later phases.
PATCH /api/v1/admin/collections/:id/challenge Admin-only. Toggle the challenge flag + set the coin reward (0-5000). Challenges are admin-curated to prevent users from self-flagging lists of already-watched anime to farm coins. AdminPATCH /api/v1/admin/collections/:id/featured Admin-only. Toggle the `is_featured` flag. Featured public collections get pinned to the top of the global browse feed. AdminDELETE /api/v1/admin/collections/comments/:comment_id Admin-only. Soft-delete any comment regardless of ownership. AdminGET /api/v1/collections List collections. Defaults to the public browse index. `user_id=<uuid>` scopes to one owner; when the caller IS that owner every visibility is returned, otherwise only public rows. `visibility=unlisted|private` is caller-scoped (always pinned to the caller, ignores any user_id). `is_public=true` is legacy sugar for `visibility=public`. Returns an array (unwrapped). POST /api/v1/collections Create a new collection owned by the caller. AuthGET /api/v1/collections/:id Get a single collection with owner info and anime_count. Returns 404 for private rows the caller does not own (existence hidden). PATCH /api/v1/collections/:id Update collection metadata. Owner-only. Any subset of the Create fields is accepted. AuthDELETE /api/v1/collections/:id Delete a collection (cascades to items). Owner-only. AuthPOST /api/v1/collections/:id/challenge/claim One-time coin reward for completing every anime in a challenge collection. Returns 400 "not complete" if progress < 100%, 409 "already claimed" with the cached progress if the caller has already claimed, 400 "not a challenge collection" if the row isn't flagged. Successful claims grant coins via the economy service and record the claim so repeats are blocked at the DB layer. AuthGET /api/v1/collections/:id/challenge/progress Caller's standing on a challenge (or any collection — non-challenge rows return is_challenge=false so the UI has one path). Completion cross-references anime_statuses where status = completed, matched via shikimori_id. Anonymous callers see only the challenge metadata with totals/counts zeroed. GET /api/v1/collections/:id/comments Newest-first comments on a collection with author profile inline. `limit` query (default 50, max 200) caps the page. Soft-deleted comments are hidden. Visibility-gated. POST /api/v1/collections/:id/comments Post a new comment. Mute-gated (muted users are blocked here same as on anime comments). Visibility-gated (no commenting on rows you can't see). AuthPOST /api/v1/collections/:id/favorite Bookmark a visible collection for the caller. Idempotent. AuthDELETE /api/v1/collections/:id/favorite Remove the caller's bookmark. Idempotent and not visibility-gated — even if the collection has since been made private, the caller can still clean up their own favourites list. AuthPOST /api/v1/collections/:id/fork Create a copy of a visible collection owned by the caller. Items and groups are cloned; the new row defaults to visibility="private" so the forker can decide whether to publish. Attribution is stored in `forked_from_id`. Optional body `{ "title": "…" }` overrides the cloned title (otherwise the source title is kept). AuthGET /api/v1/collections/:id/groups List groups (sections) inside a collection, ordered by sort_order. Honours the collection's visibility gate. Returns [] if the collection has no groups. POST /api/v1/collections/:id/groups Create a group (section) inside a collection. Owner-only. Optional `tier_label` (≤4 chars — e.g. "S", "A+") turns the section into a tier-list row; NULL means an ordinary section. AuthPATCH /api/v1/collections/:id/groups/:group_id Update a group. Owner-only. Accepts any subset of { name, description, sort_order, tier_label }. Send `tier_label: null` to explicitly clear the tier label. AuthDELETE /api/v1/collections/:id/groups/:group_id Delete a group. Owner-only. Items in the group are preserved and moved to the ungrouped bucket (ON DELETE SET NULL), not lost. AuthGET /api/v1/collections/:id/items List items in a collection, ordered by position. Honours the same visibility gate as GET /collections/:id. POST /api/v1/collections/:id/items Bulk-add anime to a collection. Owner-only. Duplicates (same anime_id) are silently skipped. `anime_id` is the Shikimori ID; `anime_title`/`anime_src` are denormalised for display without requiring the anime in our local catalog. AuthPATCH /api/v1/collections/:id/items/:item_id Update one item. Owner-only. Accepts `group_id` (null to clear) and `position`. AuthDELETE /api/v1/collections/:id/items/:item_id Remove one item. Owner-only. AuthPOST /api/v1/collections/:id/reaction Set the caller's like or dislike on a visible collection. One reaction per user — posting a different value upserts. Self-reactions (owner liking own list) are permitted. AuthDELETE /api/v1/collections/:id/reaction Remove the caller's reaction. Idempotent (succeeds even if there was no reaction to begin with). AuthGET /api/v1/collections/:id/social Aggregate social state for a collection: like/dislike counts, the caller's own reaction (null for anon), favourites count + whether the caller has favourited, and the cached view_count. Visibility-gated. POST /api/v1/collections/:id/view Bump the public view counter. OptionalAuth — anon viewers count too. Private collections only count views from the owner (others get 404). Bursts are not de-duplicated in Phase 3; the counter is social, not monetised.
DELETE /api/v1/collections/comments/:comment_id Soft-delete a comment. Allowed for the comment's author OR the collection owner. The row is retained for moderation auditing. AuthDELETE /api/v1/collections/items/:item_id Convenience form of the above — resolves the parent collection from the item id before enforcing ownership. AuthUploads 1 endpoint User-facing R2 uploads (admin uploads use /admin/r2). These routes live on the Next.js side because the R2 SDK is JS-only; the Go API does not sign or handle R2 writes.
POST /api/uploads/list-cover Upload a collection cover image to R2. 5 MB cap, images only. Stored under `collections/covers/{user_id}/`. The route verifies the caller against /auth/me before writing. AuthProgression 3 endpoints Path of Ascension: races (human, elf, vampire, ork), age accrual per watched episode, evolution, and racial passives. Accrual runs inside POST /anime-list/:anime_id/episode with a 15-minute anti-abuse cooldown. Streak resets after 6 hours of inactivity.
GET /api/v1/progression Get the authenticated user's progression snapshot. AuthPOST /api/v1/progression/evolve Evolve to a new race. Requires progression_status = "evolution_ready". Resets age_months to 120 (10 years). Valid races: human, elf, vampire, ork. Cannot evolve to the same race. AuthPOST /api/v1/progression/watch Manually accrue an episode (normally called server-side by /anime-list/:id/episode). Skipped (no-op) if last accrual was under 15 minutes ago. Flips status to "evolution_ready" when age reaches the current race cap. AuthAchievements 3 endpoints View and manage achievement unlock status.
GET /api/v1/achievements List all achievements with the current user's unlock status. AuthPOST /api/v1/achievements/check-time Trigger time-based achievement checks (e.g. watching at specific hours). AuthPOST /api/v1/achievements/mark-notified Mark achievements as notified so they stop appearing in new-unlock banners. AuthQuests 2 endpoints Daily and weekly quests with rewards.
GET /api/v1/quests List available quests with current progress. AuthPOST /api/v1/quests/:slug/claim Claim a completed quest's reward. AuthEconomy 4 endpoints Virtual currency (coins), experience points (XP), leveling, and transactions.
GET /api/v1/economy/balance Get the authenticated user's current balance. AuthPOST /api/v1/economy/daily-reward Claim the daily login reward. Can be claimed once per day; streak bonuses apply. AuthPOST /api/v1/economy/purchase [LEGACY] Generic economy purchase. Store/profile customization items should use POST /api/v1/store/purchase. AuthGET /api/v1/economy/transactions Get transaction history for the authenticated user. AuthFriends 10 endpoints Friend requests, management, and blocking.
GET /api/v1/friends List all friends and pending requests. AuthDELETE /api/v1/friends/:id Remove a friend. AuthPOST /api/v1/friends/:id/accept Accept a pending friend request. AuthPOST /api/v1/friends/:id/block Block a user. Removes existing friendship if any. AuthPOST /api/v1/friends/:id/reject Reject a pending friend request. AuthPOST /api/v1/friends/request Send a friend request to another user. Rate-limited to 30/hour per user. AuthPOST /api/v1/users/:userID/block Block a user by their UUID. Works without a prior friendship — backed by the friendships table with status='blocked'. Used by the chat user-info modal's "Заблокировать" entry. AuthDELETE /api/v1/users/:userID/block Unblock a previously-blocked user. Deletes the friendships row outright; a fresh friend request is required to re-establish contact. AuthGET /api/v1/users/:userID/friends Public list of a specific user's accepted friends. Used by the profile Friends tab — /friends always returns the caller's own friends. POST /api/v1/users/:userID/report File a moderation report against a user. Reason must be one of: spam, harassment, hate_speech, nsfw, impersonation, underage, self_harm, other. Message is optional (max 2000 chars). Rate-limited to 5/hour. Notifies every moderator/admin/superadmin. AuthWatch Together 7 endpoints Invite a friend into a watch-party from your solo watch page. The backend tracks "who is currently watching what" via heartbeat, joins that with the friends list to power the chat-sidebar "online + watching" pill, and on accept pre-creates a watch-party seeded with the sender's anime + timecode. Live delivery rides on the chat gateway (events WATCH_TOGETHER_REQUEST + WATCH_TOGETHER_RESOLVED). Invites expire after 60s; a background sweeper marks stale pending rows as expired and purges expired mutes.
POST /api/v1/watch-together/request Send a watch-together invite to a friend. Anime + timecode are pulled from the caller's presence row when they're watching, otherwise from the recipient's row (covers the chat-sidebar "ask to join" flow where the clicker is not watching). Rejects if: neither side is currently watching, not friends (accepted), recipient has toggled off, recipient muted the sender (or globally), or a pending invite already exists between the pair. AuthPOST /api/v1/watch-together/requests/:id/accept Accept a pending invite. Pre-creates a watch-party room seeded with the sender's anime + timecode, marks the request accepted with the new room_id, and fans WATCH_TOGETHER_RESOLVED out to both parties so their UIs can jump into /watch-party/<room_id>. AuthPOST /api/v1/watch-together/requests/:id/decline Decline a pending invite. Optional body field `mute` applies a temporary silence: "none" (default — just decline), "sender_1h" (1h mute from this sender only), "all_1h" (1h global mute). AuthGET /api/v1/watch/friends-activity List friends (status=accepted) who currently have a fresh presence row. Sorted newest first. Powers the chat-sidebar "друг смотрит …" section. AuthPOST /api/v1/watch/presence Heartbeat that records the caller's current watching state. Called every 15s by the solo player while playback is alive. Staleness is 2 minutes — past that the row no longer counts as "watching". AuthDELETE /api/v1/watch/presence Drop the caller's presence row immediately — used on tab close / navigation away from the player. AuthGET /api/v1/watch/users/:id/presence Returns the target user's currently-watching state if their online_status_visibility allows the viewer to see it (everyone, or friends + caller is an accepted friend, or self). Powers the "сейчас смотрит ..." pill on user profiles. Stale rows past the 2-min freshness window are hidden as if absent. AuthNotifications 4 endpoints In-app notification management.
GET /api/v1/notifications List notifications for the authenticated user. AuthPOST /api/v1/notifications/delete Delete specific notifications. Omit `ids` and pass `all:true` to clear all. AuthPOST /api/v1/notifications/mark-read Mark specific notifications as read. AuthGET /api/v1/notifications/unread-count Unread notification counts. count = all unread; attention_count = unread with alerts enabled per notification_prefs (comment_reply is excluded when the comments pref is off; absent pref = enabled). unread_count kept for backward compatibility. AuthShop 2 endpoints Browse and purchase cosmetic items with coins.
GET /api/v1/shop Browse available shop items with optional filters. POST /api/v1/shop/:id/buy Purchase an item from the shop. AuthMarketplace 3 endpoints User-to-user item trading marketplace.
GET /api/v1/marketplace Browse active marketplace listings. POST /api/v1/marketplace Create a new marketplace listing. AuthPOST /api/v1/marketplace/:id/buy Purchase a marketplace listing. AuthAdmin 60 endpoints Administrative endpoints. Requires authentication AND admin role.
GET /api/v1/admin/activity User activity log — every state-changing (POST/PATCH/DELETE) authenticated request. Superadmin-only. AdminDELETE /api/v1/admin/activity Wipe all user_activity_log rows. Superadmin-only. Logs a clear_activity_log action in admin_logs. AdminPOST /api/v1/admin/ai/chat Admin stats assistant (Gemini 2.5 Flash). One turn of the tool-use loop. Server returns 503 {error:"ai_unconfigured"} when GEMINI_API_KEY is unset, and 429 {error:"rate_limited", retry_after_seconds, message} when the upstream Gemini quota is exhausted or the model is temporarily overloaded — the frontend renders a live countdown banner from retry_after_seconds. Topic-locked to Anilay stats/moderation; tool catalog is the entire capability surface — no free-form SQL. AI is strictly read+create+edit only — destructive ops (delete/drop/truncate/purge) are refused at both the system-prompt and dispatcher layers. AdminGET /api/v1/admin/ai/conversations List the caller's recent AI chat threads, most-recently-updated first. Query: ?limit=50. AdminGET /api/v1/admin/ai/conversations/:id Fetch one conversation + full message history. 403 if it belongs to another admin. AdminPATCH /api/v1/admin/ai/conversations/:id Rename a conversation (sidebar title edit). 403 if not owner. AdminDELETE /api/v1/admin/ai/conversations/:id Delete one AI thread. 403 if not owner. Audit rows in ai_audit are preserved for the privacy trail. AdminGET /api/v1/admin/analytics Site-wide page-view analytics. Source is the /analytics/pageview beacon. AdminPOST /api/v1/admin/anime Create a new anime entry manually. AdminGET /api/v1/admin/anime-stats Per-anime engagement stats: views, list additions, completions over a time range. AdminPATCH /api/v1/admin/anime/:id Update an existing anime entry. All fields optional — only keys present in the body are written. Arrays overwrite. AdminGET /api/v1/admin/anime/:id/candidates Per-field candidate values parsed from stored Shikimori + Kodik snapshots (plus the current DB value). Lets admins compare providers side-by-side before picking a value to apply via PATCH /admin/anime/:id. AdminPATCH /api/v1/admin/anime/:id/sources Set field-level data sources for an anime (which fields come from which provider). AdminPOST /api/v1/admin/anime/:id/sync Force re-sync anime data from Shikimori and Kodik. AdminGET /api/v1/admin/deletion-requests List pending account-deletion requests awaiting superadmin review. Superadmin-only. AdminPOST /api/v1/admin/deletion-requests/:id/decide Approve or deny a deletion request. Approve hard-deletes immediately; deny cancels the request. Superadmin-only. AdminGET /api/v1/admin/digests List all weekly-digest drafts and past sends. AdminPOST /api/v1/admin/digests Create a new digest draft. Only the latest draft is picked up by the weekly cron (Monday 09:00 UTC). AdminGET /api/v1/admin/digests/:id Get a digest by ID (draft or sent). AdminPATCH /api/v1/admin/digests/:id Update a digest draft. Rows with status=sent are immutable. AdminDELETE /api/v1/admin/digests/:id Delete a digest draft. Sent digests are retained as history and cannot be deleted. AdminPOST /api/v1/admin/digests/:id/send Send a digest now to all opted-in users. Marks row as status=sent. AdminPOST /api/v1/admin/digests/:id/send-test Send a test copy of the digest only to the authenticated admin's own email. AdminGET /api/v1/admin/downloads List recent download jobs (newest first, up to `limit`). AdminPOST /api/v1/admin/downloads Enqueue a download job. The backend worker invokes ffmpeg to remux the supplied HLS/MP4 source into a local .mp4. Accepted sources: anilibria, kodik. The source URL must pass a host allowlist (SSRF guard). AdminGET /api/v1/admin/downloads/:id Poll status and progress of a single download job. AdminDELETE /api/v1/admin/downloads/:id Delete the job row and unlink the on-disk file. Safe on any status — running jobs orphan and are reclaimed by the 30min stuck-job sweep. AdminPOST /api/v1/admin/downloads/:id/ticket Mint a short-lived (10min) ticket the browser uses to stream the .mp4 via plain navigation. Validated (not consumed) on each file GET so Range requests and retry-after-dropout work. Returns 409 if the job is not yet done. AdminGET /api/v1/admin/logs Retrieve admin audit logs (moderation actions performed by admins). AdminDELETE /api/v1/admin/logs Wipe all admin_logs rows. Superadmin-only. Writes a new admin_logs entry for the clear action itself. AdminDELETE /api/v1/admin/profile-items/:id Delete a profile item from the catalog. AdminPOST /api/v1/admin/profile-items/grant Grant a profile item to a user, bypassing shop purchase. AdminPOST /api/v1/admin/profile-items/revoke Remove a profile item from a user's inventory. AdminGET /api/v1/admin/settings Get site-wide runtime settings. Restricted to admin/superadmin — the tester role is NOT allowed (it can read other admin test-data but not settings, which hold secrets). Secret values (telegram_bot_token, kodik_token, anime365_password/login, and any *_token/*_secret/*_password/*_api_key key) are returned MASKED (••••••••) when set, empty string when unset — never in cleartext. AdminPATCH /api/v1/admin/settings Update one or more site settings (admin/superadmin only; tester excluded). Secrets are WRITE-ONLY: a blank or masked (••••••••) value is ignored so it never overwrites the stored secret — the UI echoes the mask back for unchanged fields. Changing a secret value requires superadmin (403 otherwise). AdminGET /api/v1/admin/stats Aggregate user stats: signup counts, online/active cohorts (day/week/month), unique visitors today, returning users, average session length, and 14-day signup chart. AdminGET /api/v1/admin/system-errors Server-side error log (every ERROR/FATAL/PANIC zerolog event). Superadmin-only. AdminDELETE /api/v1/admin/system-errors Wipe all system_errors rows. Superadmin-only. Logs a clear_system_errors action in admin_logs. AdminGET /api/v1/admin/user-details Fetch an admin-only view of a user: flags, notes, warning history, mute/ban state, and Path of Ascension snapshot. AdminPATCH /api/v1/admin/user-details Update admin-only user metadata (internal notes, flags). AdminGET /api/v1/admin/users List users with search, filter (all/online/today/inactive/banned) and sort (created_at/last_seen_at). Legacy q/role/banned params still accepted. AdminDELETE /api/v1/admin/users/:id Hard-delete a user account immediately. For normal flow prefer the 7-day deletion-request pipeline. AdminGET /api/v1/admin/users/:id/analytics Per-user visit analytics: total views, time on site, sessions, days active, first/last visit, avg session length, avg absence between active days, hourly + daily distribution, top pages. AdminPOST /api/v1/admin/users/:id/ban Ban a user account. AdminPOST /api/v1/admin/users/:id/coins Grant coins to a user. AdminPOST /api/v1/admin/users/:id/force-logout Force logout a user by invalidating all their sessions. AdminPOST /api/v1/admin/users/:id/mute Mute a user. Blocks authoring comments, posts, and chat messages for the duration — unlike ban, does NOT revoke sessions. AdminPOST /api/v1/admin/users/:id/progression Override a user's Path of Ascension progression (race, age, status, streak, total_watched). All fields optional — only provided fields are updated. AdminPOST /api/v1/admin/users/:id/role Set a user's role. AdminPOST /api/v1/admin/users/:id/unban Unban a user account. AdminPOST /api/v1/admin/users/:id/unmute Clear an active mute on a user. AdminPOST /api/v1/admin/users/:id/warn Issue a warning to a user. AdminGET /api/v1/downloads/:id/file Stream the remuxed .mp4 with Content-Disposition: attachment. NOT under /admin because Fiber Groups install their middleware via Use(prefix,…), so every path under /admin/* is bearer-gated, and a plain <a href download> cannot send a bearer header. Auth is provided via a required `?ticket=<uuid>` query param minted from the admin-gated Ticket route; the ticket is validated (not consumed) on each hit so Range/resume works, and expires via TTL (10min). Supports Range requests (Accept-Ranges: bytes). Returns 409 if not ready, 410 if reaped, 401 if the ticket is missing/invalid. GET /api/v1/staff/anime/:id/timecodes Same payload as the public /anime/:id/timecodes endpoint, gated behind moderator-or-above auth. Lives under /staff (not /admin) because moderators can also curate timecodes. AuthPUT /api/v1/staff/anime/:id/timecodes/:ep Upsert opening/ending timecodes for one (episode, dub). The dub is selected via ?translation_id=… (default empty = the fallback row applied to any dub without its own override). NULL intro/outro pairs encode "no opening" or "no ending"; the row's mere existence is what flips the player's skip button on. To revert a (episode, dub) row to "not yet reviewed", use DELETE. Allowed for moderator/admin/tester/superadmin. AuthDELETE /api/v1/staff/anime/:id/timecodes/:ep Remove the timecodes row for one (episode, dub), returning it to the "not yet reviewed" state where the player hides the skip button. ?translation_id=… selects which dub's row; default "" deletes the fallback row. AuthPOST /api/v1/staff/anime/:id/timecodes/:ep/copy Atomic server-side copy of one (episode, dub) row's intro/outro values to another translation_id for the same episode — used to mirror correct timecodes from one dub to another without retyping. Both ?from= and ?to= are required and must differ; either may be empty (the default fallback row). 404 if the source row doesn't exist. AuthPUT /api/v1/staff/anime/by-shikimori/:sid/timecodes/:ep By-shikimori variant of the timecode upsert — used by the in-player capture widget so moderators can save while watching, without first resolving the internal anime row on the client. Also accepts ?translation_id=… (defaults to ""). AuthDELETE /api/v1/staff/anime/by-shikimori/:sid/timecodes/:ep By-shikimori variant of timecode deletion. Also accepts ?translation_id=…. AuthGET /api/v1/staff/timecodes/missing Anime that have at least one episode in their streaming_data without an admin-set timecode row. The moderation work queue. Sorted by score DESC so high-traffic gaps surface first. AuthTelemetry 2 endpoints Client-facing telemetry feeds powering the admin AI stats assistant. Heartbeats drive time-on-site; player events drive source-health rollups. Search + user-IP logs are written server-side from existing handlers/middleware and have no client endpoint.
POST /api/v1/heartbeat Minute-bucketed presence ping. Authenticated. Called by the AuthProvider hook every ~60s while a tab is active; server dedupes on (user_id, minute) so flooding is a no-op. AuthPOST /api/v1/player-events Single player lifecycle event (play | pause | complete | error | source_switch | buffer | seek). Optional auth — anonymous events still feed source-health aggregates. Unknown events are 400-rejected to keep the enum clean. Changelog 5 endpoints Site-wide release notes powering the public /changelog page and the Instagram-stories "what's new" modal that pops once on a returning user's first load after a new entry. cover_url is the 9:16 portrait video the modal autoplays muted/looped; detail_banner_url is the dedicated 16:9 landscape banner shown on /changelog/:slug when a user hits "Подробнее". The public listing falls back to cropping cover_url into a 16:9 frame when detail_banner_url is empty.
POST /api/v1/admin/changelog Publish a new entry. Super-admin only — fans CHANGELOG_PUBLISHED out via the chat WS hub so every connected client pops the stories modal without a refresh. AdminPATCH /api/v1/admin/changelog/:id Edit an existing entry. Super-admin only. Same body shape as POST. Does NOT re-broadcast over WS — silent corrections are the desired UX (otherwise every typo fix re-pops the stories on every device). AdminDELETE /api/v1/admin/changelog/:id Hard-delete an entry. Super-admin only. The stories modal already showed the row to clients that opened it; their seen-id marker may still point at the deleted id, but listChangelog won't return it again, so subsequent loads behave correctly. Returns 204 on success. AdminGET /api/v1/changelog List published entries newest-first. Public — no auth, so the modal can fetch on landing pages without a login race. GET /api/v1/changelog/:slug Fetch one entry by slug. Public.
Watch Party 2 endpoints Synchronized watch-together rooms with realtime chat. The /ws endpoint upgrades to a WebSocket.
GET /api/v1/watchparty/rooms/:id Get a preview of a watch party room (title, anime, host, current occupancy). GET /api/v1/watchparty/ws WebSocket upgrade for a watch party session. Auth is via Bearer token on the upgrade request. Guests allowed. Miscellaneous 3 endpoints Analytics beacon, support forms, and public runtime settings used by the frontend.
POST /api/v1/analytics/pageview Track a page view. Fire-and-forget beacon; does not require authentication. GET /api/v1/settings/:key Fetch a single PUBLIC site setting by key, no auth. Only the explicit allowlist is served: support_email, support_telegram_link, donate_card_number, donate_card_holder, og_title, og_description, og_image_url. Any other key — including secrets — returns 404 (not 403) so a secret's existence is never confirmed. Secrets are only reachable via the admin settings endpoint (masked). POST /api/v1/support Submit a support / contact form. Emails ops and optionally pings Telegram. Public, but sends a Bearer JWT when the user is logged in (OptionalAuth). Server-side validation: email format + <=254 chars, subject <=200, message <=5000, username <=100. Anonymous submits must pass Cloudflare Turnstile (cf_turnstile_response) when TURNSTILE_SECRET_KEY is configured — fail closed (403). Screenshots are accepted only for authenticated users or after a successful Turnstile, capped at 3 (extras dropped). Per-IP rate limit: 5/hour. Developer 3 endpoints Manage API keys for third-party integrations.
GET /api/v1/developer/keys List all API keys for the authenticated user. Keys are masked. AuthPOST /api/v1/developer/keys Create a new API key with specified scopes. AuthDELETE /api/v1/developer/keys/:id Revoke (permanently delete) an API key. AuthMobile Devices 2 endpoints Expo Push token registration for the iOS/Android app. Tokens are fanned out to the Expo Push API on every in-app notification.
DELETE /api/v1/devices/:id Unregister a device token owned by the caller (e.g. on explicit logout). AuthPOST /api/v1/devices/register Register or refresh an Expo Push token for the authenticated user. Call on login and whenever the OS issues a new token. AuthChat 26 endpoints Direct messages (E2EE via Signal-style X3DH + ratcheted AEAD), group DMs, the global announcement channel, and the support queue. Hosted under chat.anilay.com (prod) / chat-staging.anilay.com (staging). WebSocket gateway at /api/v1/chat/ws delivers realtime MESSAGE_CREATE / REACTION / TYPING / READ_RECEIPT events; REST below is the authoritative source for state-changing operations.
GET /api/v1/chat/attachments/:id Download a chat attachment by its attachment id (not r2_key). Authorization is by conversation membership — the same check as message history — so knowing an attachment id or raw r2_key does not grant access from another conversation. Missing attachment, deleted message, and forbidden access all return an identical 404. On success: 302 redirect to a <=60s presigned R2 GET (ciphertext for E2EE DMs; the client decrypts locally). Images are served inline, everything else as an attachment download; nosniff. Per-user rate limit 120/min. Prefer this over reading r2_key directly (the chat/* R2 prefix is private). AuthPOST /api/v1/chat/blocks/:userID Block another user. Two-way effect: they can't DM you and their conversations are hidden from your list. AuthDELETE /api/v1/chat/blocks/:userID Unblock a user previously blocked. AuthGET /api/v1/chat/channels/:id/pins List the (up to 50) pinned messages in a channel, newest-first. Requires VIEW_CHANNEL. AuthGET /api/v1/chat/conversations List all conversations the caller participates in. Includes the implicit global channel at the top. Unread counts derived from each member row's last_read_at. AuthPOST /api/v1/chat/conversations Open a DM or group DM. For DMs the call is idempotent — re-creating with the same peer returns the existing conversation. AuthGET /api/v1/chat/conversations/:id/messages Paginated message history in reverse-chronological order. AuthPOST /api/v1/chat/conversations/:id/messages Send a plaintext message to any conversation kind (DM / group_dm / global / support). Body must include content OR at least one attachment. The legacy DM E2EE shape (content_enc + ratchet_header) is still accepted and persisted for wire-format compatibility but is no longer required — DMs are server-readable now. Rate-limited to 20 sends / 10s per user; posting to global requires admin/moderator role. forwarded_from_message_id snapshots the original sender on the new row (Telegram-style attribution); the server walks one hop of the source's own forward chain so forwarding-a-forward attributes back to the original author. AuthGET /api/v1/chat/conversations/:id/pins List the (up to 50) pinned messages in a conversation, newest-first. Requires membership (or Global). AuthPOST /api/v1/chat/conversations/:id/read Advance the caller's read cursor to the given timestamp (or now if omitted). AuthGET /api/v1/chat/keys/:userID/bundle Fetch a peer's prekey bundle for X3DH. Atomically consumes one OTK from the peer's pool per call; when the pool is empty the bundle is returned without OneTimePrekey and clients fall back to signed-prekey-only X3DH. AuthGET /api/v1/chat/keys/me Report whether the caller has uploaded E2EE identity material and how many one-time prekeys remain. The client uses otk_remaining to decide when to top up. AuthPATCH /api/v1/chat/keys/me Upload (or rotate) the caller's Ed25519 identity pub + signed X25519 prekey + Ed25519 signature over the prekey. Server stores PUBLIC halves only. AuthPOST /api/v1/chat/keys/prekeys Bulk-insert one-time prekeys into the caller's pool. Idempotent per (user, prekey_id). AuthPATCH /api/v1/chat/messages/:id Edit a message the caller sent. Plaintext convs: supply new content. DMs: supply new content_enc + ratchet_header. AuthDELETE /api/v1/chat/messages/:id Soft-delete a message the caller sent. The row is preserved (so replies resolve) but content is elided on read. AuthPOST /api/v1/chat/messages/:id/pin Pin a message. Channel messages require MANAGE_MESSAGES on the channel. Conversation messages require membership (DMs/groups) or staff (Global). Broadcasts MESSAGE_UPDATE so every viewer sees the pin badge appear. Idempotent — re-pin refreshes pinned_by to the current actor. AuthDELETE /api/v1/chat/messages/:id/pin Unpin a previously-pinned message. Same permissions as pin. AuthPOST /api/v1/chat/messages/:id/reactions Add an emoji reaction. Idempotent. AuthDELETE /api/v1/chat/messages/:id/reactions/:emoji Remove one of the caller's own reactions. AuthGET /api/v1/chat/messages/:id/read-by Sender-only: list conversation members whose read-cursor has crossed this message. Returns empty for channel messages (channels don't track per-user reads). Capped at 50 entries, sorted by read_at ascending (earliest readers first). AuthPOST /api/v1/chat/reports Report a message, user, or conversation to the moderation queue. Rate-limited to 10/hour. AuthGET /api/v1/chat/support Return the caller's open support conversation, creating a fresh one if none is open. Used on page load to materialise the "Support" entry in the sidebar. AuthPOST /api/v1/chat/support/close Caller closes their own support ticket. AuthGET /api/v1/chat/ws WebSocket gateway. Preferred auth: ?ticket=<t> from POST /chat/ws-ticket (no JWT in the URL). Legacy ?token=<JWT> is still accepted only while WS_ALLOW_JWT_QUERY=true (transition window; will be removed). Server replies with HELLO (heartbeat_interval) + READY (user, conversations), then pushes MESSAGE_CREATE / MESSAGE_UPDATE / MESSAGE_DELETE / REACTION_ADD / REACTION_REMOVE / TYPING_START / READ_RECEIPT / PRESENCE_UPDATE / WATCH_PRESENCE_UPDATE / WATCH_TOGETHER_REQUEST / WATCH_TOGETHER_RESOLVED / VOICE_STATE_UPDATE / THREAD_ARCHIVED dispatches. Heartbeat every 30s. AuthPOST /api/v1/chat/ws-ticket Mint a single-use ticket for the chat WebSocket upgrade (requires a normal Bearer JWT). Preferred over putting the JWT in the WS URL. The ticket is a 256-bit random string, valid for 30s, one-time, bound to your user + session. Connect with /api/v1/chat/ws?ticket=<ticket>. AuthChat — Servers, Roles, Channels 69 endpoints Discord-style servers with roles, categories, channels, and per-channel permission overrides. Permission model: 64-bit bitflag (see frontend/src/lib/chat/perms.ts for the exhaustive list). Effective perms = @everyone role | user role perms → @everyone channel override → union of user-role channel overrides → user-specific channel override; ADMINISTRATOR and server ownership short-circuit to full. Cached in Redis per (user, channel) with a 60s TTL; every role / override change invalidates the relevant keys.
GET /api/v1/chat/automod/words List automod rules. Query: server_id (optional). Members of the server can read; returns server-specific + global rules when scoped, global-only when unscoped. AuthPOST /api/v1/chat/automod/words Add an automod rule. Body: { server_id?, pattern, match_kind?: "contains"|"word"|"regex", action?: "block"|"flag" }. server_id omitted = global rule (super-admin only). Per-server rules need server owner or MANAGE_SERVER. Rate-limited 30/h. AuthDELETE /api/v1/chat/automod/words/:id Remove an automod rule. Same permission model as create. AuthPATCH /api/v1/chat/channels/:id Patch a channel. Requires MANAGE_CHANNELS on the channel itself. AuthDELETE /api/v1/chat/channels/:id Delete a channel. Requires MANAGE_CHANNELS. AuthGET /api/v1/chat/channels/:id/messages Paginated channel message history. Requires VIEW_CHANNEL + READ_MESSAGE_HISTORY. Query: before (RFC3339Nano), limit (default 50, max 200), thread (uuid, optional — scopes to a single thread within the channel). AuthPOST /api/v1/chat/channels/:id/messages Post a plaintext message to a channel. Requires VIEW_CHANNEL + SEND_MESSAGES. Announcement channels additionally require MANAGE_MESSAGES. Slow-mode is enforced in Redis (SETNX with TTL); MANAGE_MESSAGES / MANAGE_CHANNELS bypass slow-mode. Rate-limited to 20 sends / 10s per user. Body may include an `attachments` array — each element carries { r2_key, mime, size_bytes, filename?, width?, height?, duration_ms?, enc_key?, enc_nonce? } where enc_key/enc_nonce are base64 AEAD material for DM attachments only. forwarded_from_message_id attributes the post as a forward; the server snapshots the original sender (and walks a one-hop chain so forward-of-forward credits the original author). AuthPUT /api/v1/chat/channels/:id/overrides/role/:roleID Set a role-level channel permission override. Body: { allow: int64, deny: int64 }. Requires MANAGE_ROLES. AuthDELETE /api/v1/chat/channels/:id/overrides/role/:roleID Remove a role override. Requires MANAGE_ROLES. AuthPUT /api/v1/chat/channels/:id/overrides/user/:userID Set a user-level channel permission override. Body: { allow: int64, deny: int64 }. Requires MANAGE_ROLES. AuthGET /api/v1/chat/channels/:id/perms/@me Compute the caller's effective permission bitmask on a channel. Returned so the frontend can grey out buttons the user cannot use. AuthGET /api/v1/chat/channels/:id/threads List threads under a channel, active first. Query: archived (bool, default false), limit (default 50, max 200). Requires VIEW_CHANNEL. AuthPOST /api/v1/chat/channels/:id/threads Open a thread under a text / announcement / forum channel. Requires VIEW_CHANNEL + CREATE_THREAD. Forum channels automatically set thread_kind=forum_post. Body: { name, auto_archive_minutes?, root_message? } — root_message seeds a first post in the same round-trip. AuthPOST /api/v1/chat/channels/:id/voice/grant-speaker/:userID Staff promotes a listener to speaker. Flips LiveKit CanPublish=true. Requires MOVE_MEMBERS or PRIORITY_SPEAKER. Clears raise-hand. AuthPOST /api/v1/chat/channels/:id/voice/mute/:userID Staff mute — flips a participant's CanPublish to false without kicking. Requires MUTE_MEMBERS. Body: { muted: bool }. AuthGET /api/v1/chat/channels/:id/voice/participants Live participant list fetched from the LiveKit admin API. Requires VIEW_CHANNEL. AuthDELETE /api/v1/chat/channels/:id/voice/participants/:userID Force-remove a participant from the voice channel. Requires MOVE_MEMBERS. AuthPOST /api/v1/chat/channels/:id/voice/raise-hand Stage channel: listener requests to be promoted to speaker. Redis-tracked pending set with 10-min TTL. Fans a VOICE_STATE_UPDATE (event=raise_hand) to staff so the UI can surface the queue. Rate-limited 10/min. AuthGET /api/v1/chat/channels/:id/voice/raised-hands Current raise-hand queue (Redis SMEMBERS). Powers the stage-moderator panel; live updates continue via VOICE_STATE_UPDATE events. Requires VIEW_CHANNEL. AuthPOST /api/v1/chat/channels/:id/voice/revoke-speaker/:userID Staff demotes a speaker back to listener. MOVE_MEMBERS or PRIORITY_SPEAKER. AuthPOST /api/v1/chat/channels/:id/voice/token Mint a short-lived LiveKit participant JWT for a voice (or stage) channel. Requires VIEW_CHANNEL + CONNECT; SPEAK gates the publish bit. Returns { token, ws_url, room, can_publish, expires_in } — the client connects to ws_url with the token via livekit-client. Rate-limited 30/min per user. Returns 503 (voice_unconfigured) when LIVEKIT_KEYS is unset. AuthGET /api/v1/chat/channels/:id/webhooks List webhooks on a channel. MANAGE_WEBHOOKS. AuthDELETE /api/v1/chat/emojis/:emojiID Delete a custom emoji. MANAGE_EMOJIS on the owning server. AuthDELETE /api/v1/chat/invites/:code Revoke an invite. Inviter, server owner, or channel MANAGE_CHANNELS. AuthPOST /api/v1/chat/invites/:code/accept Join the server the invite points at. Atomically bumps `uses` under the max/expiry guards, refuses if banned, adds the caller as a server member. Rate-limited to 20/hour per user. AuthGET /api/v1/chat/messages/:id/attachments List attachments for a message. Membership / VIEW_CHANNEL enforced against the message's parent. AuthPATCH /api/v1/chat/roles/:id Patch a role. Owner-only. @everyone cannot be renamed, repositioned, or deleted (only perms can change via this endpoint). AuthDELETE /api/v1/chat/roles/:id Delete a role. Owner-only. @everyone cannot be deleted. AuthPUT /api/v1/chat/roles/:id/members/:user Assign a role to a user. Owner-only. AuthDELETE /api/v1/chat/roles/:id/members/:user Remove a role from a user. Owner-only. AuthGET /api/v1/chat/search/inbox FTS across the caller's own server-readable conversations (group DMs, global, support). E2EE DMs are excluded — the UI surfaces a note. Query: q, limit. Rate-limited 60/min. AuthGET /api/v1/chat/servers List every server the caller is a member of. AuthPOST /api/v1/chat/servers Create a server. Caller becomes owner; an @everyone role is seeded with the default member bitmask. AuthGET /api/v1/chat/servers/:id Get a single server by id. Must be a member. AuthPATCH /api/v1/chat/servers/:id Patch name / description / icon / banner / public flag / default channel. Owner-only. AuthDELETE /api/v1/chat/servers/:id Delete the server. Cascades to roles, channels, members, overrides. Owner-only. AuthPOST /api/v1/chat/servers/:id/bans/:user Ban a user from the server. Requires BAN permission. Body: { reason?: string }. AuthDELETE /api/v1/chat/servers/:id/bans/:user Unban a previously-banned user. Owner-only. AuthGET /api/v1/chat/servers/:id/categories List categories. Members only. AuthPOST /api/v1/chat/servers/:id/categories Create a category. Owner-only. AuthGET /api/v1/chat/servers/:id/channels List channels. Members only. AuthPOST /api/v1/chat/servers/:id/channels Create a channel. Owner-only. Types: text / voice / announcement / forum / stage. AuthGET /api/v1/chat/servers/:id/emojis List custom emojis on a server. Members only. AuthPOST /api/v1/chat/servers/:id/emojis Register a custom per-server emoji. Client must have uploaded the image to R2 (via /api/r2/upload-url type=chat) and pass the r2_key. Requires MANAGE_EMOJIS. Rate-limited 30/h. AuthGET /api/v1/chat/servers/:id/invites List invites on a server. Any member can see. AuthPOST /api/v1/chat/servers/:id/invites Mint an invite code. Requires CREATE_INVITE on the target channel (or any channel when channel_id is omitted). Rate-limited to 30/hour per user. AuthPOST /api/v1/chat/servers/:id/join Discovery-join: add caller as a member of a public server (is_public=true). 403 on invite-only servers — use /invites/:code/accept instead. Idempotent. Rate-limited to 30/hour per user. AuthGET /api/v1/chat/servers/:id/members List members. Must be a member of the server. AuthDELETE /api/v1/chat/servers/:id/members/:user Kick a member. Requires KICK on any channel. AuthPATCH /api/v1/chat/servers/:id/members/@me Change your own nickname. Body: { nickname: string | null }. AuthDELETE /api/v1/chat/servers/:id/members/@me Leave a server. Owners cannot leave — transfer ownership or delete the server instead. AuthGET /api/v1/chat/servers/:id/roles List roles on a server. Members only. AuthPOST /api/v1/chat/servers/:id/roles Create a role. Owner-only. AuthGET /api/v1/chat/servers/:id/search Postgres FTS (russian dictionary) over channel messages the caller has VIEW_CHANNEL + READ_MESSAGE_HISTORY on. Query: q (required), limit (default 50, max 100). Rate-limited to 60/min per user. AuthGET /api/v1/chat/servers/:id/stickers List stickers on a server. Members only. AuthPOST /api/v1/chat/servers/:id/stickers Register a custom sticker. MANAGE_STICKERS. Same upload flow as emojis. AuthPOST /api/v1/chat/servers/:id/webhooks Create an incoming webhook. Returns a plaintext token the client should store in an external service. Requires MANAGE_WEBHOOKS on the target channel. Body: { name, channel_id, avatar_url? }. AuthGET /api/v1/chat/share/resolve Resolve an anilay URL (e.g. /anime/123, /user/alice, /collection/<uuid>, /watch-party/<roomId>) into a rich share-card payload. Used by the chat UI to render "share" messages. Cached 5 minutes per URL. Query: url (required). Returns { url, kind, title, subtitle?, description?, cover_url?, icon_url?, color?, action_label?, action_url?, extra? }. AuthDELETE /api/v1/chat/stickers/:stickerID Delete a sticker. MANAGE_STICKERS. AuthGET /api/v1/chat/threads/:id Thread metadata. Message history uses /channels/:id/messages?thread=<id>. AuthPATCH /api/v1/chat/threads/:id Patch name / locked / archived / auto_archive_minutes. Owner or MANAGE_THREADS on the parent channel. AuthDELETE /api/v1/chat/threads/:id Delete a thread. Cascades to messages. Owner or MANAGE_THREADS. AuthGET /api/v1/chat/threads/:id/members List members of a thread. Public / forum_post threads are visible to anyone with VIEW_CHANNEL; private threads require member or MANAGE_THREADS. AuthPOST /api/v1/chat/threads/:id/members/:user Add a user to a private thread. Owner or MANAGE_THREADS. AuthDELETE /api/v1/chat/threads/:id/members/:user Remove a thread member. Self-remove always allowed; kicking others requires owner or MANAGE_THREADS. AuthDELETE /api/v1/chat/webhooks/:id Delete a webhook. MANAGE_WEBHOOKS or original creator. AuthPOST /api/v1/chat/webhooks/:id/execute PUBLIC — post a message into the webhook's channel. Authenticates via Authorization: Bearer <token> header. Body: { content, username?, avatar_url? }. Overrides the webhook's default name/avatar per message if username / avatar_url are supplied. GET /api/v1/discovery Paginated list of public chat servers (`is_public=true`). PUBLIC — anonymous browse. Lives outside /chat for the same reason as /invites/:code. Query: limit (default 30, max 100), before (RFC3339Nano cursor).
GET /api/v1/invites/:code Resolve a code to its server preview. PUBLIC — no auth required so anonymous users can peek at the destination before signing in. Discord-parity path (outside the /chat prefix because Fiber installs the /chat-level Auth middleware globally for that path tree). Returns only non-sensitive server fields (name, icon, description, public flag).
Admin — Chat 3 endpoints Staff-only endpoints behind RequireAdmin(). Hosted on chat.anilay.com / chat-staging.anilay.com alongside the user-facing chat routes.
POST /api/v1/admin/chat/support/:ticketID/assign Claim a support ticket. The calling staff member is added to the conversation as a staff member. AdminPOST /api/v1/admin/chat/support/:ticketID/close Close a support ticket. Message history is retained for audit. AdminGET /api/v1/admin/chat/support/queue List open + assigned support tickets, newest-activity first. Used by the staff triage UI. AdminStore 8 endpoints Backend-driven store: catalog, purchase/claim, inventory, wishlist, featured. StoreItem JSON uses media_url (mapped from file_url), theme_payload/skin_payload, and per-viewer owned/in_wishlist.
GET /api/v1/store/featured Admin-curated featured/hero set. GET /api/v1/store/inventory Items the user owns plus coin balance. AuthGET /api/v1/store/items List enabled store items. owned/in_wishlist are annotated for the authenticated viewer. GET /api/v1/store/items/:id Single store item with viewer annotations. POST /api/v1/store/purchase Buy (coins) or claim (free) an item into inventory. Errors: 402 insufficient_balance, 404 item_not_found, 409 already_owned, 423 item_locked. AuthGET /api/v1/store/wishlist The user’s wishlist. AuthPOST /api/v1/store/wishlist/:item_id Add an item to the wishlist. AuthDELETE /api/v1/store/wishlist/:item_id Remove an item from the wishlist. AuthMerch Orders 4 endpoints Real-money merch checkout. Payment is provider-agnostic (Stub adapter now — admin confirms; YooKassa later).
GET /api/v1/store/orders The user’s orders. AuthPOST /api/v1/store/orders Create a merch order (validates stock/size, snapshots price, decrements stock) and a payment intent. Errors: 404 item_not_found, 409 out_of_stock, 422 invalid_size/not_purchasable. AuthGET /api/v1/store/orders/:id A single order (owner only). AuthPOST /api/v1/store/orders/:id/webhook Payment provider callback. Stub accepts { intent_id, status }. Profile Customization 5 endpoints Apply owned items to profile slots (background, avatar, frame, badge, theme, skin).
GET /api/v1/users/:username/customization Public profile customization (applied items only). POST /api/v1/users/me/avatar/confirm Confirm a custom avatar already uploaded to R2 (one per week). AuthPOST /api/v1/users/me/avatar/upload Confirm a custom avatar already uploaded to R2 (alias of /avatar/confirm; one per week). AuthGET /api/v1/users/me/customization Current user’s applied items per slot (with embedded StoreItem) + custom_avatar_url. AuthPATCH /api/v1/users/me/customization Apply or clear (null) slots. Applying an unowned item that is default_unlocked + published auto-grants it into inventory first (source default_unlocked); otherwise 403 item_not_owned. 409 item_not_applicable if item type mismatches the slot. AuthEpisode Social 2 endpoints Per-episode likes/dislikes and 1–10 ratings. One reaction + one score per user per (anime, episode).
GET /api/v1/anime/:anime_id/episodes/:episode_num/social Aggregate stats. my_reaction/my_score are null when unauthenticated. PUT /api/v1/anime/:anime_id/episodes/:episode_num/social Set the viewer’s reaction and score (full replace). null clears a field; both null deletes the row. AuthNews 2 endpoints Admin-authored news posts. Comments use the polymorphic comments table (target_type=news).
GET /api/v1/news Published news, newest first. GET /api/v1/news/:id A single published news post. Admin · Store 12 endpoints Store catalog + grants + featured + merch orders. Admin only.
POST /api/v1/admin/store/featured Add/position an item in a featured section. AdminDELETE /api/v1/admin/store/featured/:item_id Remove from a featured section (?section=). AdminPOST /api/v1/admin/store/grant Generic grant/revoke. AdminPOST /api/v1/admin/store/items Create a store item. AdminPATCH /api/v1/admin/store/items/:id Update a store item (presence-based). AdminDELETE /api/v1/admin/store/items/:id Soft-delete a store item (clears it from active customizations). AdminPOST /api/v1/admin/store/items/:id/grant Grant an item to a user. AdminDELETE /api/v1/admin/store/items/:id/grant/:user_id Revoke an item from a user (clears it from their active customization). AdminGET /api/v1/admin/store/items/:id/grants Current owners of an item. AdminGET /api/v1/admin/store/orders List merch orders (?status=&limit=&offset=). AdminPATCH /api/v1/admin/store/orders/:id Update order status/tracking (cancel/refund restores stock). AdminPOST /api/v1/admin/store/orders/:id/refund Refund an order (restores stock). AdminAdmin · News & Moderation 8 endpoints News CRUD, comment-report moderation queue, and episode-social reset. Admin only.
GET /api/v1/admin/anime/:id/episodes/:episode_num/social Aggregate episode social stats. AdminDELETE /api/v1/admin/anime/:id/episodes/:episode_num/social Reset all reactions/ratings for an episode. AdminGET /api/v1/admin/comment-reports Moderation queue (?status=open&page=&limit=) with comment + reporter context. AdminPATCH /api/v1/admin/comment-reports/:id Resolve a report. AdminGET /api/v1/admin/news List all news (including unpublished). AdminPOST /api/v1/admin/news Create a news post. AdminPATCH /api/v1/admin/news/:id Update a news post (presence-based). AdminDELETE /api/v1/admin/news/:id Delete a news post. AdminAnime Page Data 1 endpoint Single merged payload for the anime page (override -> local -> shikimori). Shikimori is only fetched when a critical local field is missing.
GET /api/v1/anime/page-data/:shikimoriId Resolved anime + Go-native override (custom_data + visibility) + shikimori fallback. 400 bad id, 404 nowhere, 502 resolve error. Profile Items 9 endpoints Customization catalog: published items with viewer state, server inventory, and admin gifts. Items have status (draft|published|hidden) + flags default_unlocked/free_claimable/giftable/show_in_store.
GET /api/v1/admin/profile-items Admin catalog incl. drafts/hidden. AdminPOST /api/v1/admin/profile-items Create a catalog item (status auto-syncs is_enabled). AdminPATCH /api/v1/admin/profile-items/:id Update a catalog item (status auto-syncs is_enabled). AdminPOST /api/v1/admin/profile-items/:id/gift Gift a giftable item to a user (creates a pending gift + profile_item_gift notification). 422 not_giftable. expires_at is optional — after it passes the pending gift expires and claim returns 410 gift_expired. AdminPOST /api/v1/admin/profile-items/import-r2 Create a draft item from an already-uploaded R2 file. AdminGET /api/v1/profile-items Published catalog with viewer state. GET /api/v1/profile-items/gifts The user’s pending gifts (bag), each with its item. AuthPOST /api/v1/profile-items/gifts/:gift_id/claim Claim a pending gift into inventory. 404 gift_not_found, 409 already_claimed. AuthGET /api/v1/profile-items/inventory The user’s owned items (server-side). AuthAdmin · Background Videos 3 endpoints Server-side transcoding of uploaded background videos (R2 + ffmpeg).
POST /api/v1/admin/background-videos/poster Synchronous: generate ONLY the static poster (no variants). With timestamp = an exact frame, otherwise the smart poster. force=false returns the existing poster. 400 invalid_key, 404 source_not_found, 500 ffmpeg_failed. AdminPOST /api/v1/admin/background-videos/process-one Async: poster FIRST (so a preview appears immediately), then resolution variants (@720/@1080/@1440, no upscale), uploaded back to R2. Returns 202; transcoding runs in the background. Auth: admin JWT or Bearer BACKGROUND_PROCESS_SECRET (R2-event automation). 400 invalid/variant key, 503 if R2 unconfigured. AdminGET /api/v1/admin/background-videos/status R2-existence check: which variants + poster are ready for a video (so the UI doesn’t treat one @720 as full completion). Admin
Threaded comments on anime pages with up/down voting. Muted users are blocked at /anime/:id/comments POST.