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
317 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/register Register a new user account. 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/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/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/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/verify-email Verify an email address using the code sent via email. Rate-limited to 5/min per IP. 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/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/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/:id Revoke a specific session by ID. If it matches the current token, the caller is logged out. AuthDELETE /api/v1/auth/sessions Revoke all sessions except the current one. 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/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/enable Enable 2FA after verifying a TOTP code generated from the setup secret. Returns one-time backup codes. AuthDELETE /api/v1/auth/2fa/disable Disable 2FA on the authenticated account. Requires the current password for confirmation. AuthPOST /api/v1/auth/2fa/backup-codes Regenerate backup codes. Previous codes are invalidated. AuthUsers 14 endpoints Public user profiles and search.
GET /api/v1/users/search Search users by username or display name. 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/:username Get a user's public profile by username. 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/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/deletion Get the status of any pending account deletion request for the authenticated user. AuthPOST /api/v1/users/me/deletion/request Step 1 of account deletion. Sends a confirmation code to the user's email. 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. AuthDELETE /api/v1/users/me/deletion Cancel a pending deletion request during the grace window. 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. 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. 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. AuthAnime 13 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/search Full-text search for anime titles. GET /api/v1/anime/calendar Get the weekly anime release calendar. GET /api/v1/anime/by-shikimori/:sid Resolve an anime by its Shikimori ID. Returns the same shape as GET /api/v1/anime/:id. 404 if the Shikimori ID is unknown locally. 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/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/: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/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/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/:id/similar Get a list of similar anime. GET /api/v1/anime/:id/related Get related anime (sequels, prequels, side stories — full franchise). 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/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.
Anime List 7 endpoints Manage personal anime watch lists. Track status, score, and episode progress.
GET /api/v1/users/:username/anime-list Get a user's anime list (public). 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). AuthCollections 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.
GET /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). GET /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). POST /api/v1/collections Create a new collection owned by the caller. AuthPATCH /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. 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. AuthDELETE /api/v1/collections/items/:item_id Convenience form of the above — resolves the parent collection from the item id before enforcing ownership. 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/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/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). 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/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.
POST /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). AuthPATCH /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. AdminGET /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). AuthDELETE /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/admin/collections/comments/:comment_id Admin-only. Soft-delete any comment regardless of ownership. AdminGET /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. POST /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. AuthPATCH /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. AdminUploads 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/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. 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. 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. AuthGET /api/v1/economy/transactions Get transaction history for the authenticated user. 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 Purchase an item using coins. AuthFriends 10 endpoints Friend requests, management, and blocking.
GET /api/v1/friends List all friends and pending requests. 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/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. AuthPOST /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. AuthPOST /api/v1/friends/request Send a friend request to another user. Rate-limited to 30/hour per user. AuthPOST /api/v1/friends/:id/accept Accept a pending friend request. AuthPOST /api/v1/friends/:id/reject Reject a pending friend request. AuthPOST /api/v1/friends/:id/block Block a user. Removes existing friendship if any. AuthDELETE /api/v1/friends/:id Remove a friend. 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/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/friends-activity List friends (status=accepted) who currently have a fresh presence row. Sorted newest first. Powers the chat-sidebar "друг смотрит …" section. 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. AuthPOST /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). AuthNotifications 4 endpoints In-app notification management.
GET /api/v1/notifications List notifications for the authenticated user. AuthPOST /api/v1/notifications/mark-read Mark specific notifications as read. AuthGET /api/v1/notifications/unread-count Get the count of unread notifications. AuthPOST /api/v1/notifications/delete Delete specific notifications. Omit `ids` and pass `all:true` to clear all. 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 63 endpoints Administrative endpoints. Requires authentication AND admin role.
POST /api/v1/admin/anime Create a new anime entry manually. AdminPATCH /api/v1/admin/anime/:id Update an existing anime entry. All fields optional — only keys present in the body are written. Arrays overwrite. 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/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. AdminGET /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. 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/: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. 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. AuthGET /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. AdminPOST /api/v1/admin/users/:id/ban Ban a user account. AdminPOST /api/v1/admin/users/:id/unban Unban a user account. AdminPOST /api/v1/admin/users/:id/warn Issue a warning to a user. AdminPOST /api/v1/admin/users/:id/role Set a user's role. 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/progression Override a user's Path of Ascension progression (race, age, status, streak, total_watched). All fields optional — only provided fields are updated. 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. 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/unmute Clear an active mute on a user. 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/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/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/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/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/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/analytics Site-wide page-view analytics. Source is the /analytics/pageview beacon. 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. AdminGET /api/v1/admin/anime-stats Per-anime engagement stats: views, list additions, completions over a time range. AdminGET /api/v1/admin/settings Get site-wide runtime settings (feature flags, branding, values exposed on the public settings endpoint). AdminPATCH /api/v1/admin/settings Update one or more site settings. 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. 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, anime365, kodik. The source URL must pass a host allowlist (SSRF guard). AdminGET /api/v1/admin/downloads List recent download jobs (newest first, up to `limit`). AdminGET /api/v1/admin/downloads/:id Poll status and progress of a single download job. 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/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. DELETE /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. AdminGET /api/v1/admin/profile-items List all profile items in the admin catalog (frames, badges, themes, avatars). AdminPOST /api/v1/admin/profile-items Create a new profile item. AdminPATCH /api/v1/admin/profile-items/:id Update a profile item. 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. 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. AdminTelemetry 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.
GET /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.
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. AdminProfile Items 7 endpoints User-facing catalog and inventory for frames, badges, themes, and custom avatars.
GET /api/v1/profile-items Public catalog of all profile items. GET /api/v1/profile-items/inventory Get the authenticated user's owned items. AuthGET /api/v1/profile-items/active Get the authenticated user's currently equipped items per slot. AuthPOST /api/v1/profile-items/active Equip an owned item into its slot (or pass null item_id to unequip). AuthGET /api/v1/profile-items/avatar/cooldown Get the cooldown state for uploading a custom avatar. AuthPOST /api/v1/profile-items/avatar/upload Upload a custom avatar image. Multipart request with an image file field. Rate-limited to 10/hour per user (on top of the existing avatar-change cooldown). AuthGET /api/v1/profile-items/user/:username Get another user's equipped profile items (frame/badge/theme/avatar). Watch Party 2 endpoints Synchronized watch-together rooms with realtime chat. The /ws endpoint upgrades to a WebSocket.
GET /api/v1/watchparty/ws WebSocket upgrade for a watch party session. Auth is via Bearer token on the upgrade request. Guests allowed. GET /api/v1/watchparty/rooms/:id Get a preview of a watch party room (title, anime, host, current occupancy). 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. POST /api/v1/support Submit a support / contact form. Emails ops and optionally pings Telegram. GET /api/v1/settings/:key Fetch a single public site setting by key (e.g. feature flags read by the frontend without auth). Developer 3 endpoints Manage API keys for third-party integrations.
POST /api/v1/developer/keys Create a new API key with specified scopes. AuthGET /api/v1/developer/keys List all API keys for the authenticated user. Keys are masked. 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.
POST /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. AuthDELETE /api/v1/devices/:id Unregister a device token owned by the caller (e.g. on explicit logout). AuthChat 24 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/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. AuthPOST /api/v1/chat/conversations/:id/read Advance the caller's read cursor to the given timestamp (or now if omitted). 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/reactions Add an emoji reaction. Idempotent. AuthDELETE /api/v1/chat/messages/:id/reactions/:emoji Remove one of the caller's own reactions. 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. 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/:id/pins List the (up to 50) pinned messages in a conversation, newest-first. Requires membership (or Global). 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/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. 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/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). 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/ws WebSocket gateway. Pass JWT as a query param (?token=...); browsers cannot set Authorization on an upgrade. 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. PRESENCE_UPDATE fires on a friend's first connect / last disconnect. WATCH_PRESENCE_UPDATE fires on heartbeat / clear. Heartbeat every 30s. 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.
POST /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 List every server the caller is a member of. 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. AuthDELETE /api/v1/chat/servers/:id/members/@me Leave a server. Owners cannot leave — transfer ownership or delete the server instead. 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. 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. AuthPATCH /api/v1/chat/servers/:id/members/@me Change your own nickname. Body: { nickname: string | null }. AuthPOST /api/v1/chat/servers/:id/roles Create a role. Owner-only. AuthGET /api/v1/chat/servers/:id/roles List roles on a server. Members only. 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. AuthPOST /api/v1/chat/servers/:id/categories Create a category. Owner-only. AuthGET /api/v1/chat/servers/:id/categories List categories. 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/channels List channels. Members only. 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/perms/@me Compute the caller's effective permission bitmask on a channel. Returned so the frontend can grey out buttons the user cannot use. AuthPUT /api/v1/chat/channels/:id/overrides/role/:roleID Set a role-level channel permission override. Body: { allow: int64, deny: int64 }. 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. AuthDELETE /api/v1/chat/channels/:id/overrides/role/:roleID Remove a role override. Requires MANAGE_ROLES. 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). AuthGET /api/v1/chat/messages/:id/attachments List attachments for a message. Membership / VIEW_CHANNEL enforced against the message's parent. 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. AuthGET /api/v1/chat/servers/:id/invites List invites on a server. Any member can see. AuthDELETE /api/v1/chat/invites/:code Revoke an invite. Inviter, server owner, or channel MANAGE_CHANNELS. AuthGET /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).
POST /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/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).
POST /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. 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. 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/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/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. 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/voice/participants Live participant list fetched from the LiveKit admin API. Requires VIEW_CHANNEL. 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 }. AuthDELETE /api/v1/chat/channels/:id/voice/participants/:userID Force-remove a participant from the voice channel. Requires MOVE_MEMBERS. 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? }. 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/emojis List custom emojis on a server. Members only. AuthDELETE /api/v1/chat/emojis/:emojiID Delete a custom emoji. MANAGE_EMOJIS on the owning server. AuthPOST /api/v1/chat/servers/:id/stickers Register a custom sticker. MANAGE_STICKERS. Same upload flow as emojis. AuthGET /api/v1/chat/servers/:id/stickers List stickers on a server. Members only. AuthDELETE /api/v1/chat/stickers/:stickerID Delete a sticker. MANAGE_STICKERS. 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/channels/:id/webhooks List webhooks on a channel. MANAGE_WEBHOOKS. 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/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. 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. 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/revoke-speaker/:userID Staff demotes a speaker back to listener. MOVE_MEMBERS or PRIORITY_SPEAKER. 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/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. AuthGET /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. AuthDELETE /api/v1/chat/automod/words/:id Remove an automod rule. Same permission model as create. AuthAdmin — Chat 3 endpoints Staff-only endpoints behind RequireAdmin(). Hosted on chat.anilay.com / chat-staging.anilay.com alongside the user-facing chat routes.
GET /api/v1/admin/chat/support/queue List open + assigned support tickets, newest-activity first. Used by the staff triage UI. AdminPOST /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. Admin
Threaded comments on anime pages with up/down voting. Muted users are blocked at /anime/:id/comments POST.