{"openapi":"3.1.0","info":{"title":"Ticket Tool X API","version":"1.0.0","description":"The API behind the Ticket Tool X dashboard, the Discord bot, and anything else that needs to read or change a server's ticket configuration.\n\n### How authentication works\n\nThere are no API keys. Every call is authenticated by a session cookie issued by `GET /api/auth/login`, which is a Discord OAuth round trip. The cookie is signed with HMAC-SHA256 and carries the session itself, so there is no session table and nothing to revoke centrally; it expires on its own after seven days.\n\n### How authorisation works\n\n- **Public.** No session needed. Safe to call from anywhere, and returns nothing tied to a person.\n- **Signed in.** Requires a valid session cookie. Returns only data belonging to the signed-in Discord user.\n- **Server manager.** Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.\n- **Application owner.** Requires a session and ownership of the white-label application named in the path. Ownership is the only gate; there is no shared-access model.\n- **Platform staff.** Requires a session and a specific staff permission flag, read live from the database. Anything touching a named customer also requires a written reason, which is recorded before the data is returned.\n\n### Staff permissions\n\nStaff routes each require one named flag. Nothing is implied by being staff.\n\n- `support.read` - Read customer configuration. Look at any server's panels, settings and ticket counts to diagnose a problem. Reading is logged with the reason given, and the customer can see that it happened.\n- `support.write` - Change customer configuration. Repair a broken configuration on a customer's behalf. Every change is recorded in BOTH the customer's own history and the staff audit, so it never looks like the customer did it.\n- `support.tickets` - Read ticket metadata. See ticket counts, states and timings for a server. Never message contents: those belong to the customer and transcripts are theirs.\n- `tenants.read` - List white-label tenants. See which applications are connected, who owns them, and whether their credentials are set. Never the credentials.\n- `tenants.provision` - Connect a white-label application. Register a customer's own Discord application against our infrastructure. White labelling is not self-serve: we take custody of somebody else's bot token and carry the liability under Discord's Developer Terms 12(a), so it starts with a signed agreement and a person, not a form anybody who signs in can find.\n- `tenants.suspend` - Suspend a tenant. Stop a white-label application being served, for abuse or non-payment. Reversible, and separate from deletion on purpose.\n- `billing.read` - Read entitlements. See which servers are on which plan and when it expires.\n- `billing.write` - Grant or revoke entitlements. Comp a plan, extend a trial, or revoke one. Always audited with a reason.\n- `staff.read` - See the staff list. Who has access and what they hold.\n- `staff.manage` - Grant and revoke staff access. The highest privilege in the system: it can grant itself anything. Hold it on as few accounts as possible.\n- `system.read` - Read system health. Deployment state, binding presence and counts. No customer data.\n\n### Things this API will never do\n\n- Return a bot token or client secret, at any permission level, to anyone. They are encrypted at rest and reported only as set or not set.\n- Return ticket message contents to platform staff. Those belong to the customer.\n- Let a staff member read a named customer's data without recording a written reason first.","contact":{"name":"Ticket Tool X"}},"servers":[{"url":"https://tickettoolx.lunarwerx.com"}],"tags":[{"name":"Service"},{"name":"Reference"},{"name":"Authentication"},{"name":"Servers"},{"name":"Configuration"},{"name":"Panels"},{"name":"Backup"},{"name":"Web ticketing"},{"name":"White label"},{"name":"Staff"}],"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"ttx_session","description":"Set by the Discord OAuth flow. HttpOnly, SameSite=Lax, Secure in production. Not readable by JavaScript, and not something a client sets by hand."}}},"paths":{"/api/health":{"get":{"operationId":"get-health","summary":"Liveness and binding presence","description":"Confirms the deployment is up and reports which bindings are configured, as booleans. Reported rather than assumed, so a deployment missing a secret says so here instead of failing later with a confusing 500. Never reports a value.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Service"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/reference/variables":{"get":{"operationId":"get-reference-variables","summary":"Every template variable and modifier","description":"The ~54 placeholders and 11 chainable modifiers usable in any message, embed, channel name or custom command. Served from the same source the bot renders with, so the documentation cannot describe a variable the engine does not have.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Reference"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/reference/permissions":{"get":{"operationId":"get-reference-permissions","summary":"Discord permissions the bot requests, and why","description":"Each permission with the reason it is wanted, plus the ones deliberately never requested. The invite integer is derived from this list at runtime rather than stored, so it cannot disagree with what is documented here.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Reference"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/reference/staff-permissions":{"get":{"operationId":"get-reference-staff-permissions","summary":"The staff permission catalogue","description":"Every staff flag with its label and what it actually allows. Public because the security model is not a secret: publishing it is how it gets reviewed.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Reference"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/invite":{"get":{"operationId":"get-invite","summary":"Build the bot invite link","description":"Returns the OAuth invite URL for whichever application is in scope, so a white-label tenant's link carries THEIR client id. Permissions come from the named bit list at runtime, never a stored integer.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Reference"],"parameters":[{"name":"guild","in":"query","required":false,"schema":{"type":"string"},"description":"Preselect a server in Discord's install dialog."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/auth/login":{"get":{"operationId":"get-auth-login","summary":"Begin Discord sign-in","description":"302s to the unified LunarWerx auth service, which owns the only Discord redirect URI in the system. Carries a PKCE challenge whose verifier stays in an HttpOnly cookie on this deployment, so the code that comes back is worthless to anyone who intercepts it.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Authentication"],"parameters":[{"name":"next","in":"query","required":false,"schema":{"type":"string"},"description":"Path to land on afterwards. Must start with `/`; anything else is ignored, so this cannot become an open redirect."},{"name":"app","in":"query","required":false,"schema":{"type":"string"},"description":"Application id, to sign in against a specific white-label tenant."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/auth/return":{"get":{"operationId":"get-auth-return","summary":"The auth service hands a finished login back here","description":"Redeems the one-time code server to server, using a PKCE verifier that never leaves this worker, then verifies the identity token's signature, algorithm, issuer, audience and expiry before believing a single claim. Sets the session cookie and lands the person where they were going.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Authentication"],"parameters":[{"name":"code","in":"query","required":true,"schema":{"type":"string"},"description":"The one-time code. Single-use, two-minute lifetime, useless without the verifier."},{"name":"next","in":"query","required":false,"schema":{"type":"string"},"description":"Where to land, carried through the round trip."},{"name":"state","in":"query","required":false,"schema":{"type":"string"},"description":"The application id, so a host serving several tenants knows which one this login was for."}],"responses":{"200":{"description":"Success."},"302":{"description":"`auth_error` - Anything failed. Redirects to the dashboard with the reason in `error`, rather than showing a page here."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/auth/logout":{"get":{"operationId":"get-auth-logout","summary":"Clear the session","description":"Clear the session\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Authentication"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/auth/me":{"get":{"operationId":"get-auth-me","summary":"The signed-in person, and the tenant's branding","description":"Answers 200 with `authenticated: false` rather than 401 when there is no session, because the dashboard shell calls this before it knows whether anyone is signed in and a 401 there is not an error.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Authentication"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/guilds":{"get":{"operationId":"get-guilds","summary":"Servers you can configure","description":"Servers where you hold Manage Server, split by whether the bot is actually in them. The split comes from asking Discord where the bot is, not from a stored flag, because a bot removed from a server never tells us and a cached flag would offer Manage on a server it has left.\n**Access:** Signed in. Requires a valid session cookie. Returns only data belonging to the signed-in Discord user.","tags":["Servers"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}":{"get":{"operationId":"get-guilds-by-guild","summary":"Everything needed to paint a server's home screen","description":"Everything needed to paint a server's home screen\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Servers"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/directory":{"get":{"operationId":"get-guilds-by-guild-directory","summary":"The server's channels and roles","description":"So every picker offers real names instead of an id box. Answers with empty lists and a `reason` when the bot is not in the server, which is a normal state during setup rather than an error.\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Servers"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/audit":{"get":{"operationId":"get-guilds-by-guild-audit","summary":"This server's settings history","description":"Who changed what, with a structured diff. A save that changed nothing is not recorded.\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Servers"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."},{"name":"limit","in":"query","required":false,"schema":{"type":"string"},"description":"Up to 500. Defaults to 100."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/settings/{section}":{"get":{"operationId":"get-guilds-by-guild-settings-by-section","summary":"Read one settings section","description":"Sections are named by the dashboard's schema registry. Reading is allowed on any plan, including sections the server's plan cannot save, so an expired server can still see what it configured.\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."},{"name":"section","in":"path","required":true,"schema":{"type":"string"},"description":"Settings section name, from the dashboard's schema registry, for example `access` or `claiming`."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]},"post":{"operationId":"post-guilds-by-guild-settings-by-section","summary":"Save one settings section","description":"Save one settings section\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."},{"name":"section","in":"path","required":true,"schema":{"type":"string"},"description":"Settings section name, from the dashboard's schema registry, for example `access` or `claiming`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"data\": { ... } }"}},"description":"{ \"data\": { ... } }"},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"402":{"description":"`premium_required` - The section is Premium and the server is on the free plan. Enforced here, not only greyed out in the dashboard."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/panels":{"get":{"operationId":"get-guilds-by-guild-panels","summary":"List panels","description":"List panels\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]},"post":{"operationId":"post-guilds-by-guild-panels","summary":"Create a panel","description":"Create a panel\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"name\": \"Support\", \"data\": { ... } }"}},"description":"{ \"name\": \"Support\", \"data\": { ... } }"},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/panels/{panel}":{"get":{"operationId":"get-guilds-by-guild-panels-by-panel","summary":"Read one panel in full","description":"Read one panel in full\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."},{"name":"panel","in":"path","required":true,"schema":{"type":"string"},"description":"Panel id, as returned by the panels list."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]},"patch":{"operationId":"patch-guilds-by-guild-panels-by-panel","summary":"Update a panel","description":"Update a panel\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."},{"name":"panel","in":"path","required":true,"schema":{"type":"string"},"description":"Panel id, as returned by the panels list."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"name\": \"...\", \"enabled\": true, \"data\": { ... } }"}},"description":"{ \"name\": \"...\", \"enabled\": true, \"data\": { ... } }"},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]},"delete":{"operationId":"delete-guilds-by-guild-panels-by-panel","summary":"Delete a panel","description":"Its configuration goes. Tickets already opened from it are untouched and keep working.\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."},{"name":"panel","in":"path","required":true,"schema":{"type":"string"},"description":"Panel id, as returned by the panels list."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/panels/{panel}/post":{"post":{"operationId":"post-guilds-by-guild-panels-by-panel-post","summary":"Post the panel into a channel","description":"Publishes the panel where people will press it. Uses the same renderer as the slash command, so the panel you configured and the panel that appears cannot disagree.\n\nRe-posting EDITS the existing message rather than adding a second one, unless it was deleted in Discord, in which case a fresh one is posted. Omit the channel to re-post where it already is.\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Panels"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."},{"name":"panel","in":"path","required":true,"schema":{"type":"string"},"description":"Panel id, as returned by the panels list."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"channelId\": \"...\" }"}},"description":"{ \"channelId\": \"...\" }"},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."},"502":{"description":"`could_not_post` - Discord refused. A 403 means the bot lacks View Channel, Send Messages or Embed Links there."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/commands":{"get":{"operationId":"get-guilds-by-guild-commands","summary":"List custom commands","description":"List custom commands\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]},"post":{"operationId":"post-guilds-by-guild-commands","summary":"Create or update a custom command","description":"Create or update a custom command\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"name\": \"faq\", \"enabled\": true, \"group\": null, \"data\": { ... } }"}},"description":"{ \"name\": \"faq\", \"enabled\": true, \"group\": null, \"data\": { ... } }"},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/commands/{item}":{"delete":{"operationId":"delete-guilds-by-guild-commands-by-item","summary":"Delete a custom command","description":"Delete a custom command\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."},{"name":"item","in":"path","required":true,"schema":{"type":"string"},"description":"Item id within a collection, as returned by that collection's list."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/automations":{"get":{"operationId":"get-guilds-by-guild-automations","summary":"List automation rules","description":"Readable on any plan; writing needs Premium, so an expired server can still see its rules.\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]},"post":{"operationId":"post-guilds-by-guild-automations","summary":"Create or update an automation rule","description":"Create or update an automation rule\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"name\": \"...\", \"trigger\": \"interval\", \"data\": { ... } }"}},"description":"{ \"name\": \"...\", \"trigger\": \"interval\", \"data\": { ... } }"},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"402":{"description":"`premium_required` - Automation is Premium on this server."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/automations/{item}":{"delete":{"operationId":"delete-guilds-by-guild-automations-by-item","summary":"Delete an automation rule","description":"Delete an automation rule\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Configuration"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."},{"name":"item","in":"path","required":true,"schema":{"type":"string"},"description":"Item id within a collection, as returned by that collection's list."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/export":{"get":{"operationId":"get-guilds-by-guild-export","summary":"Export the whole configuration","description":"Panels, settings, custom commands and automation rules. Tickets are excluded, permanently and deliberately: a backup file gets emailed around, and real conversations in one is an incident waiting for an occasion.\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Backup"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/import":{"post":{"operationId":"post-guilds-by-guild-import","summary":"Import a configuration","description":"Settings are overwritten; panels and commands are added alongside what is there. Premium sections are skipped rather than failing the whole import, so a config from a paid server still brings everything it legitimately can.\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Backup"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"A document produced by the export route, with `version: 1`."}},"description":"A document produced by the export route, with `version: 1`."},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/guilds/{guild}/transfer-key":{"post":{"operationId":"post-guilds-by-guild-transfer-key","summary":"Mint a one-time transfer key","description":"Carries this configuration to another server without a file changing hands. The key IS the capability, so it is single-use and time-boxed.\n**Access:** Server manager. Requires a session AND Manage Server in the target server, checked live against Discord on every request rather than cached, so a demoted admin loses access immediately.","tags":["Backup"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/web/tickets":{"options":{"operationId":"options-web-tickets","summary":"CORS preflight for the widget","description":"Answered only for an origin on the server's allow-list, so a site that is not permitted finds out here rather than after posting somebody's message into the void.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Web ticketing"],"parameters":[{"name":"guild","in":"query","required":true,"schema":{"type":"string"},"description":"The server the widget is opening tickets for."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}},"post":{"operationId":"post-web-tickets","summary":"Open a ticket from a website","description":"Creates a real Discord channel for the support team and returns a key the visitor's browser keeps so they can follow the conversation without an account.\n\nPublic by necessity: the caller is a stranger on somebody's marketing site. The browser-set `Origin` header, checked against an exact allow-list, is what bounds who may call it -- there is no key, because a key embedded in a public page is not a key. That bounds abuse to sites the customer listed; it is not a claim about who the visitor is.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Web ticketing"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"guildId\": \"...\", \"subject\": \"...\", \"message\": \"...\", \"email\": \"...\", \"name\": \"...\" }"}},"description":"{ \"guildId\": \"...\", \"subject\": \"...\", \"message\": \"...\", \"email\": \"...\", \"name\": \"...\" }"},"responses":{"200":{"description":"Success."},"400":{"description":"`email_required` - The server asks for an email address and none was given."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"`origin_not_allowed` - The calling site is not on the allow-list, or web ticketing is off. Deliberately the same answer for both."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/web/tickets/{key}":{"get":{"operationId":"get-web-tickets-by-key","summary":"Read a web ticket's conversation","description":"The key is an HMAC over the ticket id, not the id itself, because an id is a number people try incrementing. Replies are attributed as `support` rather than by Discord username: an agent's identity is not the visitor's to collect.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Web ticketing"],"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"key"}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}},"post":{"operationId":"post-web-tickets-by-key","summary":"Reply to a web ticket","description":"Relayed into the Discord channel with mentions disabled, so a visitor cannot make the bot ping a role by typing one.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["Web ticketing"],"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"key"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"message\": \"...\" }"}},"description":"{ \"message\": \"...\" }"},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"409":{"description":"`ticket_closed` - The ticket is no longer open."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/white-label/agreement":{"get":{"operationId":"get-white-label-agreement","summary":"The service provider agreement","description":"The versioned text a tenant must accept before we may hold their credentials, as Discord's Developer Terms section 12(a) requires. Public so it can be read before signing up.\n**Access:** Public. No session needed. Safe to call from anywhere, and returns nothing tied to a person.","tags":["White label"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}}}},"/api/white-label/apps":{"get":{"operationId":"get-white-label-apps","summary":"Applications you own","description":"Applications you own\n**Access:** Signed in. Requires a valid session cookie. Returns only data belonging to the signed-in Discord user.","tags":["White label"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]},"post":{"operationId":"post-white-label-apps","summary":"Connect a customer's Discord application","description":"Verifies the token against Discord and confirms it belongs to the application id given, then seals both credentials with AES-256-GCM before writing. The agreement is checked first, before a credential is even looked at.\n\nStaff-only, deliberately. Taking custody of somebody else's bot token puts us on the hook under Discord's Developer Terms 12(a), so this begins with a signed agreement and a named owner rather than a form any signed-in account can reach. The required reason is where the pointer to that agreement lives, and it lands in the staff audit.\n**Access:** Platform staff. Requires a session and a specific staff permission flag, read live from the database. Anything touching a named customer also requires a written reason, which is recorded before the data is returned.\n\n**Required staff permission:** `tenants.provision`","tags":["White label"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"applicationId\": \"...\", \"name\": \"...\", \"botToken\": \"...\", \"clientSecret\": \"...\", \"acceptAgreementVersion\": \"...\", \"ownerUserId\": \"...\", \"reason\": \"...\" }"}},"description":"{ \"applicationId\": \"...\", \"name\": \"...\", \"botToken\": \"...\", \"clientSecret\": \"...\", \"acceptAgreementVersion\": \"...\", \"ownerUserId\": \"...\", \"reason\": \"...\" }"},"responses":{"200":{"description":"Success."},"400":{"description":"`reason_required` - No reason was given, so there would be no record of why this tenant exists."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"412":{"description":"`agreement_required` - The agreement version was not accepted, or is out of date."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/white-label/apps/{id}":{"get":{"operationId":"get-white-label-apps-by-id","summary":"One application, and its setup checklist","description":"Credentials appear as booleans only. Never masked, because a mask still leaks a length.\n**Access:** Application owner. Requires a session and ownership of the white-label application named in the path. Ownership is the only gate; there is no shared-access model.","tags":["White label"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Discord application id of a white-label tenant."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]},"patch":{"operationId":"patch-white-label-apps-by-id","summary":"Update branding, or rotate credentials","description":"A credential field that is absent leaves the stored value alone, so a form that submits only what changed cannot clear what it did not mention.\n**Access:** Application owner. Requires a session and ownership of the white-label application named in the path. Ownership is the only gate; there is no shared-access model.","tags":["White label"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Discord application id of a white-label tenant."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]},"delete":{"operationId":"delete-white-label-apps-by-id","summary":"Disconnect and erase","description":"Credentials are nulled in the same statement that closes the account, so there is no window where a disconnected tenant still has a usable token in the table. Required in these terms by section 12(a).\n**Access:** Application owner. Requires a session and ownership of the white-label application named in the path. Ownership is the only gate; there is no shared-access model.","tags":["White label"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Discord application id of a white-label tenant."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/white-label/apps/{id}/state":{"get":{"operationId":"get-white-label-apps-by-id-state","summary":"What Discord actually holds for this application","description":"Read live from Discord, not from our copy. The application and the bot user are two different records, and renaming one does not rename the other.\n**Access:** Application owner. Requires a session and ownership of the white-label application named in the path. Ownership is the only gate; there is no shared-access model.","tags":["White label"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Discord application id of a white-label tenant."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/white-label/apps/{id}/identity":{"post":{"operationId":"post-white-label-apps-by-id-identity","summary":"Set the bot's username, avatar or banner","description":"Sends ONLY the fields named. Discord treats a present-but-empty field as a deletion, which is how the developer portal destroys banners.\n**Access:** Application owner. Requires a session and ownership of the white-label application named in the path. Ownership is the only gate; there is no shared-access model.","tags":["White label"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Discord application id of a white-label tenant."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"username\": \"...\", \"avatar\": \"data:image/png;base64,...\", \"banner\": \"...\" }"}},"description":"{ \"username\": \"...\", \"avatar\": \"data:image/png;base64,...\", \"banner\": \"...\" }"},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"429":{"description":"`discord_rejected` - Username changes are capped at two an hour, and the portal shares that budget."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/white-label/apps/{id}/domains":{"post":{"operationId":"post-white-label-apps-by-id-domains","summary":"Claim a custom dashboard hostname","description":"Stored unverified and inert. A hostname that resolved a tenant before control of it was proven would be a takeover: point a DNS record at us and inherit whoever is already there.\n**Access:** Application owner. Requires a session and ownership of the white-label application named in the path. Ownership is the only gate; there is no shared-access model.","tags":["White label"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Discord application id of a white-label tenant."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"hostname\": \"support.yourbrand.com\" }"}},"description":"{ \"hostname\": \"support.yourbrand.com\" }"},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/staff/me":{"get":{"operationId":"get-staff-me","summary":"Am I staff, and what do I hold?","description":"Answers for any signed-in person, including `isStaff: false`. Session-level rather than staff-level on purpose: a console has to be able to ask before it knows.\n**Access:** Signed in. Requires a valid session cookie. Returns only data belonging to the signed-in Discord user.","tags":["Staff"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/staff/members":{"get":{"operationId":"get-staff-members","summary":"Who has staff access","description":"Who has staff access\n**Access:** Platform staff. Requires a session and a specific staff permission flag, read live from the database. Anything touching a named customer also requires a written reason, which is recorded before the data is returned.\n\n**Required staff permission:** `staff.read`","tags":["Staff"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/staff/members/{userId}":{"put":{"operationId":"put-staff-members-by-userId","summary":"Grant or change staff access","description":"Unknown flags are rejected rather than filtered, so a console cannot appear to grant something it did not. A caller cannot modify their own row.\n**Access:** Platform staff. Requires a session and a specific staff permission flag, read live from the database. Anything touching a named customer also requires a written reason, which is recorded before the data is returned.\n\n**Required staff permission:** `staff.manage`","tags":["Staff"],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"},"description":"Discord user id."},{"name":"reason","in":"query","required":true,"schema":{"type":"string"},"description":"Why you are doing this, at least 8 characters. Recorded in the staff audit before any data is returned. May also be sent as a body field on write routes."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"permissions\": [\"support.read\"], \"display\": \"...\", \"reason\": \"...\" }"}},"description":"{ \"permissions\": [\"support.read\"], \"display\": \"...\", \"reason\": \"...\" }"},"responses":{"200":{"description":"Success."},"400":{"description":"`cannot_modify_your_own_access` - The target is the caller. Self-escalation is refused."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]},"delete":{"operationId":"delete-staff-members-by-userId","summary":"Revoke staff access","description":"Revoke staff access\n**Access:** Platform staff. Requires a session and a specific staff permission flag, read live from the database. Anything touching a named customer also requires a written reason, which is recorded before the data is returned.\n\n**Required staff permission:** `staff.manage`","tags":["Staff"],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"},"description":"Discord user id."},{"name":"reason","in":"query","required":true,"schema":{"type":"string"},"description":"Why you are doing this, at least 8 characters. Recorded in the staff audit before any data is returned. May also be sent as a body field on write routes."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/staff/audit":{"get":{"operationId":"get-staff-audit","summary":"What staff have done, and why","description":"Readable by anyone with `staff.read`, deliberately: an audit only one person can see is not a check on that person.\n**Access:** Platform staff. Requires a session and a specific staff permission flag, read live from the database. Anything touching a named customer also requires a written reason, which is recorded before the data is returned.\n\n**Required staff permission:** `staff.read`","tags":["Staff"],"parameters":[{"name":"staff","in":"query","required":false,"schema":{"type":"string"},"description":"Filter to one staff member's actions."},{"name":"guild","in":"query","required":false,"schema":{"type":"string"},"description":"Filter to one server."},{"name":"limit","in":"query","required":false,"schema":{"type":"string"},"description":"Up to 500. Defaults to 100."}],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/staff/guilds/{guild}":{"get":{"operationId":"get-staff-guilds-by-guild","summary":"Read a customer's configuration to diagnose a problem","description":"Cross-tenant: works without holding Manage Server in the target. The reason is recorded BEFORE the data is gathered, so an attempt that later errors is still on the record. Returns ticket COUNTS, never message contents.\n**Access:** Platform staff. Requires a session and a specific staff permission flag, read live from the database. Anything touching a named customer also requires a written reason, which is recorded before the data is returned.\n\n**Required staff permission:** `support.read`","tags":["Staff"],"parameters":[{"name":"guild","in":"path","required":true,"schema":{"type":"string"},"description":"Discord server (guild) id, a snowflake of 15 to 25 digits."},{"name":"reason","in":"query","required":true,"schema":{"type":"string"},"description":"Why you are doing this, at least 8 characters. Recorded in the staff audit before any data is returned. May also be sent as a body field on write routes."}],"responses":{"200":{"description":"Success."},"400":{"description":"`reason_required` - No reason given, or shorter than 8 characters."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/staff/tenants":{"get":{"operationId":"get-staff-tenants","summary":"Every connected white-label application","description":"Credentials appear as booleans. There is no flag at any level that returns a tenant's token.\n**Access:** Platform staff. Requires a session and a specific staff permission flag, read live from the database. Anything touching a named customer also requires a written reason, which is recorded before the data is returned.\n\n**Required staff permission:** `tenants.read`","tags":["Staff"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/staff/tenants/{id}/suspend":{"post":{"operationId":"post-staff-tenants-by-id-suspend","summary":"Suspend or restore a tenant","description":"Reversible, and separate from deletion. Our own first-party application cannot be suspended.\n**Access:** Platform staff. Requires a session and a specific staff permission flag, read live from the database. Anything touching a named customer also requires a written reason, which is recorded before the data is returned.\n\n**Required staff permission:** `tenants.suspend`","tags":["Staff"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Discord application id of a white-label tenant."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"example":"{ \"suspended\": true, \"reason\": \"...\" }"}},"description":"{ \"suspended\": true, \"reason\": \"...\" }"},"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}},"/api/staff/system":{"get":{"operationId":"get-staff-system","summary":"Platform counts and deployment state","description":"Contains no customer data at all.\n**Access:** Platform staff. Requires a session and a specific staff permission flag, read live from the database. Anything touching a named customer also requires a written reason, which is recorded before the data is returned.\n\n**Required staff permission:** `system.read`","tags":["Staff"],"parameters":[],"responses":{"200":{"description":"Success."},"400":{"description":"The request was malformed. The body names the specific `error` code."},"401":{"description":"No valid session. Sign in at `/api/auth/login`."},"403":{"description":"Authenticated, but not permitted. Staff routes answer this identically whether the caller is not staff or merely lacks the flag."},"404":{"description":"No such route, or no such resource."},"405":{"description":"The path exists under other methods. The `Allow` header lists them."},"500":{"description":"An unhandled error. Nothing was changed."}},"security":[{"sessionCookie":[]}]}}}}