{"openapi":"3.1.0","info":{"title":"Doughmination API","version":"2.0.0","description":"Combined Discord presence + profile/badges API, Minecraft/Hypixel lookups, git contribution heatmaps, and the Doughmination plural-system API — one Cloudflare Worker.\n\nAll JSON responses share one envelope: `{ success, data }` or `{ success: false, error: { code, message } }`.\n\n**Realtime:** live updates (presence, fronting, mental state, devices) are pushed over a single WebSocket at `wss://doughmination.uk/v2/ws` — see /docs for the frame protocol. WebSocket endpoints are not listed as paths below.\n\n**Terms:** be reasonable with request volume or your IP gets blocked — https://doughmination.uk/terms","contact":{"email":"abuse@doughmination.win","url":"https://doughmination.uk/abuse"},"termsOfService":"https://doughmination.uk/terms"},"servers":[{"url":"https://doughmination.uk","description":"Production. Most routes live under /v2 (included in the paths below)."}],"externalDocs":{"url":"https://doughmination.uk/docs","description":"Human-readable API reference"},"paths":{"/v2/discord/users/{id}":{"get":{"tags":["Discord — profiles, presence & guilds"],"summary":"A user's full record in one call: profile (avatar, banner, badges, connected accounts, and — when available — bio, prono","description":"A user's full record in one call: profile (avatar, banner, badges, connected accounts, and — when available — bio, pronouns and reviews), the collectibles they have equipped (nameplate, profile frame, profile effect, avatar decoration — each resolved to its name and image assets), and their live presence (status, activities, Spotify, platforms). Fetched fresh on every request. Presence is null if the user shares no tracked server with the bot.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"Discord user ID.","schema":{"type":"string"}},{"name":"fresh","in":"query","required":false,"description":"Force fresh sub-resource lookups (collectibles/wishlist) instead of their short shared caches.","schema":{"type":"string"}},{"name":"nocache","in":"query","required":false,"description":"Force fresh sub-resource lookups (collectibles/wishlist) instead of their short shared caches.","schema":{"type":"string"}},{"name":"refresh","in":"query","required":false,"description":"Force fresh sub-resource lookups (collectibles/wishlist) instead of their short shared caches.","schema":{"type":"string"}}]}},"/v2/discord/users":{"get":{"tags":["Discord — profiles, presence & guilds"],"summary":"The same full record for up to 100 users at once","description":"The same full record for up to 100 users at once. Returns a map of user ID → record (or null if not found).","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"ids","in":"query","required":false,"description":"Comma-separated Discord user IDs, max 100.","schema":{"type":"string"}}]}},"/v2/discord/status":{"get":{"tags":["Discord — profiles, presence & guilds"],"summary":"Live-presence connection health: whether it's connected, how many users are tracked, and recent reconnect activity.","description":"Live-presence connection health: whether it's connected, how many users are tracked, and recent reconnect activity.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v2/discord/guilds/{invite}":{"get":{"tags":["Discord — profiles, presence & guilds"],"summary":"Public guild info resolved from an invite code: name, icon/banner/splash, member + online counts.","description":"Public guild info resolved from an invite code: name, icon/banner/splash, member + online counts.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"invite","in":"path","required":true,"description":"Invite code (the part after discord.gg/).","schema":{"type":"string"}}]}},"/v2/discord/girls/{idType}/{id}":{"get":{"tags":["Discord — profiles, presence & guilds"],"summary":"Look up a role or a member inside the 'girls' server.","description":"Look up a role or a member inside the 'girls' server.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"idType","in":"path","required":true,"description":"'role' or 'member'.","schema":{"type":"string"}},{"name":"id","in":"path","required":true,"description":"The role ID or member (user) ID.","schema":{"type":"string"}}]}},"/v2/minecraft/general/{uuid}":{"get":{"tags":["Minecraft"],"summary":"A player's Mojang identity: username, UUID (both dashed and short forms), skin and cape texture URLs, the skin's arm mod","description":"A player's Mojang identity: username, UUID (both dashed and short forms), skin and cape texture URLs, the skin's arm model (classic or slim), every cape the player has across providers (Minecraft, OptiFine, MinecraftCapes, LabyMod, 5zig, TLauncher, SkinMC — plus a custom 'doughmination' cape for hand-picked accounts) via the capes array, and a set of ready-to-embed mc-heads render images (face, head, body, player, combo, skin) — each render includes the overlay/hat layer, with _flat variants for the inner skin only. Returns 404 if no Minecraft account has that UUID.\n\nExample: GET /minecraft/general/853c80ef3c3749fdaa49938b674adae6  →  { name, uuid, skin_url, cape_url, skin_model, capes: [ { source, cape_url } ], render: { face, head, body, player, combo, skin } }","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"uuid","in":"path","required":true,"description":"Minecraft UUID — 32 hex characters, dashes optional.","schema":{"type":"string"}},{"name":"fresh","in":"query","required":false,"description":"Skip the cache and fetch fresh.","schema":{"type":"string"}},{"name":"nocache","in":"query","required":false,"description":"Skip the cache and fetch fresh.","schema":{"type":"string"}},{"name":"refresh","in":"query","required":false,"description":"Skip the cache and fetch fresh.","schema":{"type":"string"}}]}},"/v2/minecraft/capes":{"get":{"tags":["Minecraft"],"summary":"The set of vanilla (Mojang) capes the API has seen, persisted to memory","description":"The set of vanilla (Mojang) capes the API has seen, persisted to memory. It grows over time: every account looked up via /minecraft/general has its equipped vanilla cape remembered, deduped by texture hash. Third-party capes (OptiFine, LabyMod, etc.) are loaded fresh per request and are not persisted here.\n\nExample: GET /minecraft/capes  →  { count, capes: [ { source: \"minecraft\", cape_url } ] }","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v2/minecraft/hypixel/{uuid}":{"get":{"tags":["Minecraft"],"summary":"Hypixel stats for one of the operator's OWN Minecraft accounts: the player object (network level and per-game stats — Be","description":"Hypixel stats for one of the operator's OWN Minecraft accounts: the player object (network level and per-game stats — Bedwars, SkyWars, Duels, and the rest) plus their SkyBlock profiles. Per Hypixel API policy this only serves the operator's allowlisted accounts — any other UUID returns 403. Cache-first with a 5-minute TTL (no manual cache-bypass). Always returns 200 for an allowed account; the 'source' field says whether each section loaded ('ok') or why it's null ('not_found' if the account never joined Hypixel, 'unavailable' if Hypixel data isn't configured). Not affiliated with or endorsed by Hypixel Inc.\n\nExample: GET /minecraft/hypixel/853c80ef3c3749fdaa49938b674adae6  →  { name, player: {…}, skyblock: [{…}], source: { player: \"ok\", skyblock: \"ok\" } }","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"uuid","in":"path","required":true,"description":"Minecraft UUID (32 hex, dashes optional) — must be one of the operator's own accounts.","schema":{"type":"string"}}]}},"/v2/plural/login":{"post":{"tags":["Plural — accounts & auth"],"summary":"Log in with your username and password (plus a Turnstile captcha token)","description":"Log in with your username and password (plus a Turnstile captcha token). Returns an access token to use on protected endpoints.\n\nExample: POST body: { \"username\": \"admin\", \"password\": \"…\", \"turnstile_token\": \"…\" }  →  { \"access_token\": \"…\", \"token_type\": \"bearer\", \"success\": true }","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/signup":{"post":{"tags":["Plural — accounts & auth"],"summary":"Create a non-admin account","description":"Create a non-admin account. Body { username, password (≥10 chars), email, display_name?, turnstile_token }. Email is required and unique. The account starts UNCONFIRMED: login is refused until the emailed link is used, and unconfirmed accounts are deleted after 24 hours. The response includes a short-lived correction_token — hold onto it, it's the only way to fix a mistyped address without a password.\n\nExample: POST body: { \"username\": \"clove\", \"password\": \"…\", \"email\": \"you@example.com\", \"turnstile_token\": \"…\" }  →  { \"success\": true, \"email_sent\": true, \"correction_token\": \"…\", \"user\": { … } }","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/verify-email":{"post":{"tags":["Plural — accounts & auth"],"summary":"Confirm an address using the token from the email","description":"Confirm an address using the token from the email. Body { token }. Single use, valid 24 hours. Also applies a pending email change.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/resend-verification":{"post":{"tags":["Plural — accounts & auth"],"summary":"Send the confirmation link again","description":"Send the confirmation link again. Body { turnstile_token } plus either { correction_token } or { username, password }. Rate limited to one per IP per minute.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/correct-email":{"post":{"tags":["Plural — accounts & auth"],"summary":"Fix a mistyped address before confirmation, without a password","description":"Fix a mistyped address before confirmation, without a password. Body { correction_token, email, turnstile_token }. The correction token is issued only to the client that signed up, lasts 2 hours, and stops working once the account is confirmed — it is what stops anyone repointing an unconfirmed account at their own inbox.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/users/check-username":{"get":{"tags":["Plural — accounts & auth"],"summary":"Check username availability","description":"Check username availability. Returns { username, exists, available }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"username","in":"query","required":false,"schema":{"type":"string"}}]}},"/v2/plural/users/check-email":{"get":{"tags":["Plural — accounts & auth"],"summary":"Check email availability","description":"Check email availability. Returns { email, exists, available }. Rate limited to 20 lookups per IP per minute.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"email","in":"query","required":false,"schema":{"type":"string"}}]}},"/v2/plural/forgot-password":{"post":{"tags":["Plural — accounts & auth"],"summary":"Request a password-reset link by USERNAME","description":"Request a password-reset link by USERNAME. Body { username, turnstile_token }. The link is emailed to the address on file for that account; the response reports a masked hint so the user knows which inbox to check. Returns 404 for an unknown username and 409 if the account has no email on file. Rate limited to one request per IP per minute.\n\nExample: POST body: { \"username\": \"clove\", \"turnstile_token\": \"…\" }  →  { \"success\": true, \"message\": \"Reset link sent. The link expires in 15 minutes.\", \"sent_to\": \"a•••@d•••.win\" }","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/forgot-username":{"post":{"tags":["Plural — accounts & auth"],"summary":"Recover a forgotten username by EMAIL","description":"Recover a forgotten username by EMAIL. Body { email, turnstile_token }. The username is emailed to that address rather than returned, so knowing an address isn't enough to learn the username on it. Returns 404 if the address isn't registered. Rate limited to one request per IP per minute.\n\nExample: POST body: { \"email\": \"you@example.com\", \"turnstile_token\": \"…\" }  →  { \"success\": true, \"message\": \"We've sent your username to that address.\", \"sent_to\": \"y•••@e•••.com\" }","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/reset-password/check":{"get":{"tags":["Plural — accounts & auth"],"summary":"Check whether a reset token is still usable before rendering the form","description":"Check whether a reset token is still usable before rendering the form. Returns { valid }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string"}}]}},"/v2/plural/reset-password":{"post":{"tags":["Plural — accounts & auth"],"summary":"Set a new password using a token from the reset email","description":"Set a new password using a token from the reset email. Body { token, new_password (≥10 chars), turnstile_token }. Tokens last 15 minutes, are single-use, and requesting a new one revokes the previous.\n\nExample: POST body: { \"token\": \"…\", \"new_password\": \"…\", \"turnstile_token\": \"…\" }  →  { \"success\": true, \"message\": \"Your password has been reset. …\" }","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/user_info":{"get":{"tags":["Plural — accounts & auth"],"summary":"The current authenticated user (no password hash).","description":"The current authenticated user (no password hash).","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/plural/auth/is_admin":{"get":{"tags":["Plural — accounts & auth"],"summary":"{ isAdmin } for the current user.","description":"{ isAdmin } for the current user.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/plural/auth/is_owner":{"get":{"tags":["Plural — accounts & auth"],"summary":"{ isOwner } for the current user.","description":"{ isOwner } for the current user.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/plural/auth/is_pet":{"get":{"tags":["Plural — accounts & auth"],"summary":"{ isPet } for the current user.","description":"{ isPet } for the current user.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/plural/system":{"get":{"tags":["Plural — system & mental state"],"summary":"PluralKit system info (name, description, tag) with the current mental_state attached.","description":"PluralKit system info (name, description, tag) with the current mental_state attached.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v2/plural/mental-state":{"get":{"tags":["Plural — system & mental state"],"summary":"Current mental state: { level, updated_at, notes }.","description":"Current mental state: { level, updated_at, notes }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"tags":["Plural — system & mental state"],"summary":"Update mental state","description":"Update mental state. Body { level, notes? }. Broadcasts a mental_state_update over /ws.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/members":{"get":{"tags":["Plural — members & tags"],"summary":"All members enriched with tags and custom status.","description":"All members enriched with tags and custom status.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v2/plural/member/{member_id}":{"get":{"tags":["Plural — members & tags"],"summary":"One member by id OR name (case-insensitive), enriched with tags + status.","description":"One member by id OR name (case-insensitive), enriched with tags + status.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"member_id","in":"path","required":true,"description":"PluralKit member id or name.","schema":{"type":"string"}}]}},"/v2/plural/member-tags":{"get":{"tags":["Plural — members & tags"],"summary":"The full member → tags map.","description":"The full member → tags map.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/plural/member-tags/{member_identifier}":{"post":{"tags":["Plural — members & tags"],"summary":"Replace a member's tag list","description":"Replace a member's tag list. Body: string[] (JSON array of tags).","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"member_identifier","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/member-tags/{member_identifier}/add":{"post":{"tags":["Plural — members & tags"],"summary":"Add one tag","description":"Add one tag. Body { tag }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"member_identifier","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/member-tags/{member_identifier}/{tag}":{"delete":{"tags":["Plural — members & tags"],"summary":"Remove one tag from a member.","description":"Remove one tag from a member.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"member_identifier","in":"path","required":true,"schema":{"type":"string"}},{"name":"tag","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}]}},"/v2/plural/members/{member_identifier}/status":{"get":{"tags":["Plural — members & tags"],"summary":"A member's custom status ({ text, emoji, updated_at }) or null.","description":"A member's custom status ({ text, emoji, updated_at }) or null.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"member_identifier","in":"path","required":true,"schema":{"type":"string"}}]},"post":{"tags":["Plural — members & tags"],"summary":"Set/update a member's status","description":"Set/update a member's status. Body { text (≤100 chars), emoji? }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"member_identifier","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}},"delete":{"tags":["Plural — members & tags"],"summary":"Clear a member's status.","description":"Clear a member's status.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"member_identifier","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}]}},"/v2/plural/fronters":{"get":{"tags":["Plural — fronting"],"summary":"Current fronters, enriched with tags + status","description":"Current fronters, enriched with tags + status. { members: [...] }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v2/plural/switch":{"post":{"tags":["Plural — fronting"],"summary":"Set the front to a list","description":"Set the front to a list. Body { members: string[] }. Broadcasts fronters_update.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/switch_front":{"post":{"tags":["Plural — fronting"],"summary":"Switch to a single fronter","description":"Switch to a single fronter. Body { member_id }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/multi_switch":{"post":{"tags":["Plural — fronting"],"summary":"Switch to several fronters with detailed feedback","description":"Switch to several fronters with detailed feedback. Body { member_ids: string[] }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/users":{"get":{"tags":["Plural — user management"],"summary":"List all users (no password hashes).","description":"List all users (no password hashes).","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Plural — user management"],"summary":"Create a user","description":"Create a user. Body { username, password, email?, display_name?, is_admin?, is_pet? }. Accounts created here are pre-confirmed — the address is being assigned deliberately — so they can log in immediately and are never touched by the unconfirmed-account sweep.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/users/{user_id}":{"put":{"tags":["Plural — user management"],"summary":"Update a user (admin, or the user themselves)","description":"Update a user (admin, or the user themselves). Body may include display_name, current_password + new_password, avatar_url (an EXTERNAL image URL — uploads were removed), is_admin, is_pet, email. EMAIL RULES: a user may change their own address but must supply current_password, and the new address is held in pending_email until confirmed (the old one keeps working, and gets an alert). The owner may set anyone's address outright. A plain admin may NOT change someone else's.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}},"delete":{"tags":["Plural — user management"],"summary":"Delete a user","description":"Delete a user. Cannot delete yourself or the owner.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}]}},"/v2/plural/metrics/fronting-time":{"get":{"tags":["Plural — metrics"],"summary":"Per-member fronting time across 24h/48h/5d/7d/30d windows and totals.","description":"Per-member fronting time across 24h/48h/5d/7d/30d windows and totals.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}]}},"/v2/plural/metrics/switch-frequency":{"get":{"tags":["Plural — metrics"],"summary":"Switch counts per window and average switches/day.","description":"Switch counts per window and average switches/day.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}]}},"/v2/plural/admin/refresh":{"post":{"tags":["Plural — realtime & admin"],"summary":"Broadcast a force_refresh to every connected /ws client.","description":"Broadcast a force_refresh to every connected /ws client.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/bot/health":{"get":{"tags":["Plural — bot API"],"summary":"Liveness check.","description":"Liveness check.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/plural/bot/system/info":{"get":{"tags":["Plural — bot API"],"summary":"System info wrapped as { success, data }.","description":"System info wrapped as { success, data }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/plural/bot/members":{"get":{"tags":["Plural — bot API"],"summary":"Members with tags + status.","description":"Members with tags + status.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/plural/bot/fronters":{"get":{"tags":["Plural — bot API"],"summary":"Current fronters.","description":"Current fronters.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/plural/bot/switch":{"post":{"tags":["Plural — bot API"],"summary":"Switch fronters with validation","description":"Switch fronters with validation. Body { member_ids: string[] }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/bot/token/regenerate":{"post":{"tags":["Plural — bot API"],"summary":"No longer available — returns 410","description":"No longer available — returns 410. The bot token is now set manually by the server operator.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/plural/robots.txt":{"get":{"tags":["Plural — SEO"],"summary":"robots.txt.","description":"robots.txt.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v2/plural/sitemap.xml":{"get":{"tags":["Plural — SEO"],"summary":"Sitemap generated from the member list.","description":"Sitemap generated from the member list.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v2/devices":{"get":{"tags":["Devices"],"summary":"All devices → { device, level, charging, lowPowerMode, wifi, watch, airpods, updated_at }.","description":"All devices → { device, level, charging, lowPowerMode, wifi, watch, airpods, updated_at }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"tags":["Devices"],"summary":"Report device state","description":"Report device state. Only 'device' is required; supplied fields are updated, the rest untouched. Send the X-Battery-Key header.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"device","in":"query","required":false,"description":"1–64 chars (required).","schema":{"type":"string"}},{"name":"level","in":"query","required":false,"description":"Optional. Integer 0–100.","schema":{"type":"string"}},{"name":"charging","in":"query","required":false,"description":"Optional. 1 (true) or 0 (false).","schema":{"type":"string"}},{"name":"lpm","in":"query","required":false,"description":"Optional. 1 (true) or 0 (false) → lowPowerMode.","schema":{"type":"string"}},{"name":"wifi","in":"query","required":false,"description":"Optional. Any string (network name), ≤128 chars.","schema":{"type":"string"}},{"name":"watch","in":"query","required":false,"description":"Optional. 1 (connected) or 0 (not connected).","schema":{"type":"string"}},{"name":"airpods","in":"query","required":false,"description":"Optional. 1 (connected) or 0 (not connected).","schema":{"type":"string"}}],"security":[{"batteryKey":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}},"delete":{"tags":["Devices"],"summary":"Delete a device's state","description":"Delete a device's state. Returns 404 if the device doesn't exist. Send the X-Battery-Key header.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"device","in":"query","required":false,"description":"1–64 chars (required).","schema":{"type":"string"}}],"security":[{"batteryKey":[]}]}},"/v2/devices/{device}":{"get":{"tags":["Devices"],"summary":"One device, or 404.","description":"One device, or 404.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"device","in":"path","required":true,"schema":{"type":"string"}}]}},"/v2/guestbook":{"get":{"tags":["Guestbook"],"summary":"List entries, newest first → { entries: [{ id, name, message, website, ts }], total, limit, offset }.","description":"List entries, newest first → { entries: [{ id, name, message, website, ts }], total, limit, offset }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"limit","in":"query","required":false,"description":"Optional. 1–200, default 50.","schema":{"type":"string"}},{"name":"offset","in":"query","required":false,"description":"Optional. ≥0, default 0.","schema":{"type":"string"}}]},"post":{"tags":["Guestbook"],"summary":"Sign the guestbook","description":"Sign the guestbook. Honeypot (url2) + Turnstile (when configured) + per-IP rate limit (60s). Returns { ok, entry } with the new UID.\n\nExample: POST body: { \"name\": \"Clove\", \"message\": \"hi!\", \"website\": \"https://…\", \"turnstileToken\": \"…\" }","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/guestbook/{id}":{"delete":{"tags":["Guestbook"],"summary":"Delete an entry by its UID","description":"Delete an entry by its UID. Returns 404 if not found. Send the X-Battery-Key header.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"The entry's UID (required).","schema":{"type":"string"}}],"security":[{"batteryKey":[]}]}},"/v2/guestbook/import":{"post":{"tags":["Guestbook"],"summary":"Migration import: insert an entry with a fresh UID, bypassing captcha + rate limit","description":"Migration import: insert an entry with a fresh UID, bypassing captcha + rate limit. Send the X-Battery-Key header. Body { name, message, website? }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"batteryKey":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/v2/system-data/helper":{"post":{"tags":["System-data — visitor logs"],"summary":"Log a page visit (the frontend pings this on navigation)","description":"Log a page visit (the frontend pings this on navigation). Body { path? }.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}}},"get":{"tags":["System-data — visitor logs"],"summary":"Log a page visit via a plain GET, for cases where a POST isn't convenient.","description":"Log a page visit via a plain GET, for cases where a POST isn't convenient.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"path","in":"query","required":false,"schema":{"type":"string"}}]}},"/v2/system-data":{"get":{"tags":["System-data — visitor logs"],"summary":"HTML log viewer (single page).","description":"HTML log viewer (single page).","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/system-data/api/stats":{"get":{"tags":["System-data — visitor logs"],"summary":"Totals, unique IPs, last-24h, top paths.","description":"Totals, unique IPs, last-24h, top paths.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/system-data/api/recent":{"get":{"tags":["System-data — visitor logs"],"summary":"Recent visits.","description":"Recent visits.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}]}},"/v2/system-data/api/by-ip/{ip}":{"get":{"tags":["System-data — visitor logs"],"summary":"Visits from an IP.","description":"Visits from an IP.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"ip","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}]}},"/v2/system-data/api/by-path":{"get":{"tags":["System-data — visitor logs"],"summary":"Visits matching a path substring.","description":"Visits matching a path substring.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}]}},"/v2/system-data/api/suspicious":{"get":{"tags":["System-data — visitor logs"],"summary":">20 hits/hour from one IP.","description":">20 hits/hour from one IP.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/system-data/api/entry/{id}":{"get":{"tags":["System-data — visitor logs"],"summary":"One full log row.","description":"One full log row.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}]}},"/v2/contribapi":{"get":{"tags":["Contrib"],"summary":"A single contribution heatmap that merges activity from GitHub and Codeberg","description":"A single contribution heatmap that merges activity from GitHub and Codeberg. Updated at most hourly.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/":{"get":{"tags":["Meta"],"summary":"Service info (JSON) — the namespace map, plus docs/health/abuse/terms/privacy links.","description":"Service info (JSON) — the namespace map, plus docs/health/abuse/terms/privacy links.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v2/health":{"get":{"tags":["Meta"],"summary":"Liveness: Discord gateway connection state + tracked-user count + SYSTEM DO reachability","description":"Liveness: Discord gateway connection state + tracked-user count + SYSTEM DO reachability. Returns HTTP 200 when everything is up, 503 when degraded — point your uptime monitor here.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/docs":{"get":{"tags":["Meta"],"summary":"This page.","description":"This page.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/openapi.json":{"get":{"tags":["Meta"],"summary":"Machine-readable OpenAPI 3.1 spec generated from the same catalogue that renders this page.","description":"Machine-readable OpenAPI 3.1 spec generated from the same catalogue that renders this page.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/abuse":{"get":{"tags":["Meta"],"summary":"Abuse & contact page: how to report abuse, request data removal / opt-out, or disclose a vulnerability.","description":"Abuse & contact page: how to report abuse, request data removal / opt-out, or disclose a vulnerability.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/terms":{"get":{"tags":["Meta"],"summary":"Terms of service — the short version: be reasonable with request volume or your IP gets blocked.","description":"Terms of service — the short version: be reasonable with request volume or your IP gets blocked.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/privacy":{"get":{"tags":["Meta"],"summary":"Privacy page: what's logged (visitor IPs, guestbook entries, cached Discord data), why, and how to get it removed.","description":"Privacy page: what's logged (visitor IPs, guestbook entries, cached Discord data), why, and how to get it removed.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/.well-known/security.txt":{"get":{"tags":["Meta"],"summary":"RFC 9116 security contact file","description":"RFC 9116 security contact file. Also available at /security.txt.","responses":{"200":{"description":"Success envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"4XX":{"description":"Error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"JWT from POST /v2/plural/login (24h expiry) for user endpoints; the operator-set bot token (plus a 'User-Agent: CloveShortcuts/<version>' header) for /v2/plural/bot/* endpoints. Some routes additionally require an admin, owner, or pet role."},"batteryKey":{"type":"apiKey","in":"header","name":"X-Battery-Key","description":"Device key for reporting/deleting device state and managing guestbook entries."}},"schemas":{"Envelope":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"description":"Endpoint-specific payload."}}},"ErrorEnvelope":{"type":"object","required":["success","error"],"properties":{"success":{"type":"boolean","const":false},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}}}}