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 PUT PATCH DELETE
All Authenticated Public
365 endpoints Service health, platform utilities, uploads, telemetry, and mobile integration.
Health 2 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.
Platform & Tools 11 Client-facing telemetry for active-session progression and player/source diagnostics. Heartbeats drive presence-minute progression; player events provide source-health diagnostics. Search and user-IP logs are written server-side from existing handlers and middleware. Manage API keys for third-party integrations. Analytics beacon, support forms, and public runtime settings used by the frontend. Expo Push token registration for the iOS/Android app. Tokens are fanned out to the Expo Push API on every in-app notification. 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. AuthPOST /api/v1/analytics/pageview Track a page view. Fire-and-forget beacon; does not require authentication. 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. AuthDELETE /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. AuthPOST /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. 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. Registration, sessions, tokens, OAuth, verification, password recovery, and 2FA.
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. User accounts, public profiles, customization, and profile inventory.
Users 15 Public user profiles and search. Current-user and lookup endpoints. /users/me returns the full user (profile + settings + customization) — use it instead of /auth/me in the UI.
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). GET /api/v1/users/by-id/:id Public profile by user id (for chats/modals/messages). GET /api/v1/users/me Full current user (same shape as /auth/me). AuthPATCH /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/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 Legacy progress deletion endpoint. With ?anime_id=… clears one anime's rows; without it, clears every progress row for the user. The current web UI does not expose this 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.
Profile Customization 16 Apply owned items to profile slots (background, avatar, frame, badge, theme, skin). Customization catalog, gifts and administration. Public catalog and ownership use Store endpoints. Admin writes use one canonical customization contract.
GET /api/v1/admin/customization/items Admin catalog including draft and hidden items. AdminPOST /api/v1/admin/customization/items Create one complete customization item. Backend derives category, currency, is_free and is_enabled. AdminPUT /api/v1/admin/customization/items/:id Replace every editor-owned field with the same complete payload used by create. AdminDELETE /api/v1/admin/customization/items/:id Soft-delete a customization item and clear active usage. AdminPOST /api/v1/admin/customization/items/:id/gifts Gift a giftable item to a user. Creates a pending gift; expires_at is optional. AdminPOST /api/v1/admin/customization/items/:id/grants Grant a customization item directly to a user. AdminDELETE /api/v1/admin/customization/items/:id/grants/:user_id Revoke a customization item from a user. AdminGET /api/v1/admin/customization/rarities List collection points assigned to item rarities. AdminPUT /api/v1/admin/customization/rarities Replace item rarity point settings and recalculate owner collections. AdminGET /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/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. avatar_url/avatar_source describe the active avatar; custom_avatar_url is the latest personal upload kept in the avatar library. 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. AuthAnime catalog data, page payloads, user lists, and episode social activity.
Catalog & Page Data 13 Browse, search, and get detailed anime information. All endpoints are public and responses are cached. Single merged payload for the anime page (override -> local -> stored Shikimori snapshot). A live Shikimori request is made when the snapshot is missing or still lacks a critical field.
GET /api/v1/anime List anime with pagination and filters. GET /api/v1/anime/:id Get public anime metadata by internal ID. Streaming/provider fields and the internal moderation note are removed for non-staff callers. Returns 410 Gone when the caller cannot see a hidden or auth-required row; admins/testers/superadmins bypass that visibility filter. External aggregate rating fields may be present. 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 the gated episode/stream payload. Returns 410 Gone when the caller cannot see the title and 403 `{ "error": "player restricted" }` for player-restricted rows. Internal moderation notes are never returned to non-staff callers. 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 public anime metadata by Shikimori ID. Same visibility and field-stripping rules as GET /anime/:id. 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/page-data/:shikimoriId Resolved anime + Go-native override (custom_data + visibility) + shikimori fallback. 400 bad id, 404 nowhere, 502 resolve error. GET /api/v1/anime/search Full-text search for anime titles.
Anime Lists 8 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). The first page includes complete watch `stats`; later pages omit the aggregate. If only that aggregate fails, the list still returns with `stats_error: "watch_stats_unavailable"`. 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. 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.
Episode Social 2 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. AuthPlayer progress, achievements, quests, coins, and rewards.
Progress 3 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. Auth
Achievements 3 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. Auth
Quests 2 Weekly staged quest chains with coin rewards.
GET /api/v1/quests List available quests with current progress. AuthPOST /api/v1/quests/:slug/claim Claim a completed quest's reward. Auth
Economy 4 Virtual currency (coins), daily rewards, 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. Weekdays grant 50 coins; weekends grant 100 coins. 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. AuthShared rooms, synchronization, presence, and watch-party controls.
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. AuthGET /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. Obtain a one-time ticket from POST /watchparty/ws-ticket and pass it in the query string. AuthPOST /api/v1/watchparty/ws-ticket Issue a short-lived, single-use Watch Party WebSocket ticket. Send the access token in the Authorization header; the JWT is never placed in the WebSocket URL. AuthMessaging, attachments, servers, roles, channels, and moderation tools.
Messages 26 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>. Auth
Servers, Roles & Channels 69 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).
Shop catalog, marketplace transactions, inventory, and merch orders.
Shop & Marketplace 13 Browse and purchase cosmetic items with coins. User-to-user item trading marketplace. 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/marketplace Browse active marketplace listings. POST /api/v1/marketplace Create a new marketplace listing. AuthPOST /api/v1/marketplace/:id/buy Purchase a marketplace listing. AuthGET /api/v1/shop Browse available shop items with optional filters. POST /api/v1/shop/:id/buy Purchase an item from the shop. AuthGET /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. Auth
Merch Orders 4 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 }.
Core 52 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. 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. 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. Auth
Chat 3 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. Admin
Store 12 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). Admin
News & Moderation 8 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. Admin
Background Videos 3 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). AdminPOST /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.