{"openapi":"3.1.0","info":{"title":"Relay Control Plane","version":"0.1.0"},"paths":{"/.well-known/oauth-authorization-server":{"get":{"summary":"As Metadata","operationId":"as_metadata__well_known_oauth_authorization_server_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-protected-resource/mcp":{"get":{"summary":"Protected Resource Metadata","description":"Served at BOTH RFC 9728 locations: the path-suffixed form the MCP\nspec lists first and Claude probes first, and the root form ChatGPT's\ndocumentation names. Same document; the 401 challenge points at the\nsuffixed one.","operationId":"protected_resource_metadata__well_known_oauth_protected_resource_mcp_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"summary":"Protected Resource Metadata","description":"Served at BOTH RFC 9728 locations: the path-suffixed form the MCP\nspec lists first and Claude probes first, and the root form ChatGPT's\ndocumentation names. Same document; the 401 challenge points at the\nsuffixed one.","operationId":"protected_resource_metadata__well_known_oauth_protected_resource_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/register":{"post":{"summary":"Register Client","operationId":"register_client_oauth_register_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"options":{"summary":"Register Preflight","operationId":"register_preflight_oauth_register_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/authorize":{"get":{"summary":"Authorize Page","operationId":"authorize_page_oauth_authorize_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Authorize Decide","operationId":"authorize_decide_oauth_authorize_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/token":{"post":{"summary":"Token Endpoint","operationId":"token_endpoint_oauth_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"options":{"summary":"Token Preflight","operationId":"token_preflight_oauth_token_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/revoke":{"post":{"summary":"Revoke Endpoint","description":"RFC 7009: always 200, even for unknown tokens (no oracle). Only\nkind='oauth' access tokens are revocable here - a settings-page PAT\npresented to this endpoint is deliberately untouched.","operationId":"revoke_endpoint_oauth_revoke_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"options":{"summary":"Revoke Preflight","operationId":"revoke_preflight_oauth_revoke_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/oauth/connections":{"get":{"summary":"List Connections","operationId":"list_connections_api_oauth_connections_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Revoke Connection","description":"Revoke EVERYTHING a client holds for this user: access tokens and\nrefresh families. client_id arrives as a query param (CIMD ids are\nURLs; a path segment would need double-encoding).","operationId":"revoke_connection_api_oauth_connections_delete","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/healthz":{"get":{"summary":"Healthz","description":"Liveness for the compose healthcheck: 200 only when startup launched\nthe background loops AND every one of them is still running.\ntask.done() catches death (exception or return), not hangs; a per-loop\nheartbeat registry is the documented next step if hung-but-alive ever\nbites. Exposes loop NAMES only, no user data.\n\nReplaces the older /healthz that counted active users via a DB query:\nthat answered 200 with every loop dead (process-up-only, the audit\nfinding), loaded every active user row per hit, and published the\nactive-user count on an unauthenticated route. GET+HEAD and the \"ok\"\nkey are kept for whatever external monitor may already poll this.","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"summary":"Healthz","description":"Liveness for the compose healthcheck: 200 only when startup launched\nthe background loops AND every one of them is still running.\ntask.done() catches death (exception or return), not hangs; a per-loop\nheartbeat registry is the documented next step if hung-but-alive ever\nbites. Exposes loop NAMES only, no user data.\n\nReplaces the older /healthz that counted active users via a DB query:\nthat answered 200 with every loop dead (process-up-only, the audit\nfinding), loaded every active user row per hit, and published the\nactive-user count on an unauthenticated route. GET+HEAD and the \"ok\"\nkey are kept for whatever external monitor may already poll this.","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/o/{token}":{"get":{"summary":"Serve Overlay Wrapper","operationId":"serve_overlay_wrapper_o__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/raw/{token}":{"get":{"summary":"Serve Overlay Raw","operationId":"serve_overlay_raw_raw__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/unfollow":{"get":{"summary":"Page Unfollow Confirm","description":"Confirmation page. GET must not consume the action (email\nscanners prefetch links); the button POSTs.","operationId":"page_unfollow_confirm_unfollow_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Page Unfollow Consume","operationId":"page_unfollow_consume_unfollow_post","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/auth":{"post":{"summary":"Mediamtx Auth","operationId":"mediamtx_auth_auth_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ffmpeg-args":{"get":{"summary":"Ffmpeg Args","operationId":"ffmpeg_args_ffmpeg_args_get","parameters":[{"name":"path","in":"query","required":true,"schema":{"type":"string","title":"Path"}},{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/fanout-failed":{"post":{"summary":"Fanout Failed","description":"start.sh calls this when it can't fan out (no destinations, or none\nwith a decryptable URL) instead of exiting straight into a silent\nrunOnReadyRestart loop -- the 2026-07-28 no_dests incident was 83\nrelaunches in ~7 minutes with zero signal anywhere. Always logged and\nalways recorded (for the dashboard's fanout_issue field, see\napi_stream_status); admin is alerted once per cooldown once the failure\nlooks sustained rather than a one-off race at stream start.\n\nThe path is never logged (it carries the stream key) -- only the same\none-way hash start.sh already prints as hash=..., so the two logs\ncorrelate without exposing anything.","operationId":"fanout_failed_fanout_failed_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FanoutFailedPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/fanout-leg-died":{"post":{"summary":"Fanout Leg Died","description":"start.sh v3 (2026-09-19) calls this from a leg's supervisor every time\nthat destination's ffmpeg exits while the SOURCE is still live, just\nbefore relaunching it. Durable only (fanout_leg_events, migration 0075):\nno in-memory state, no user email, no admin alert here. The hourly\nmonitor (chk_fanout_leg_deaths) and the weekly report count the rows per\nuser; the user-facing signal stays the existing destination-status /\ndrop-email path, whose copy now says the connection dropped on our side\nand is being reconnected.\n\nThe path is never logged (it carries the stream key); the hash start.sh\nprints correlates the two logs. dest_id is trusted only if it parses as\na UUID AND belongs to the resolved user; otherwise stored NULL.","operationId":"fanout_leg_died_fanout_leg_died_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FanoutLegDiedPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/stream-end":{"post":{"summary":"Mediamtx Stream End","description":"MediaMTX runOnNotReady webhook. Closes the active session,\naccrues duration to the start-month usage period, fires 80%/100%\nwarning emails on threshold crossings. Reuses AuthPayload because\nMediaMTX sends the same JSON shape; we only need `path`.","operationId":"mediamtx_stream_end_stream_end_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/stream-metrics":{"post":{"summary":"Internal Stream Metrics","description":"Internal-only live-bitrate snapshot for the CALLING token's own user,\nfor the keyless MCP server (a separate process that cannot read this\nprocess's in-memory enforcement state). Returns ONLY {streaming,\ncurrent_mbps, cap_mbps} -- a strict subset of enforcement.get_user_metrics\nwith NO stream key, NO destination URL, NO MediaMTX path. Locked behind\nthe internal-read secret (require_internal_secret, distinct from the\nrelay hook secret) and the same Caddy @internal_only matcher, so it is\nnever reachable from the public internet.\n\n2026-07-30: user_id is resolved SERVER-SIDE from the caller's own bearer\ntoken (payload.token, POST body -- never a query string, mirroring\n/internal/resolve-token's own discipline), via the exact same\ntokens.resolve_token every other PAT-authenticated path goes through.\nPreviously took a bare user_id query param, trusting the caller's claim\nwith only require_internal_secret as the gate; safety depended entirely\non every MCP tool sourcing that claim from its own resolved identity and\nnever accepting one as an argument -- a property enforced by discipline,\nnot by this endpoint. There is no user_id parameter left to mismatch now,\nbecause there is no user_id parameter. See the multi-tenant authorization\naudit in docs/AUDIT-LOG.md.\n\nuptime / is-live are derived MCP-side from StreamSession (the MCP container\nreads the DB directly); only bitrate has to come over this hop because it\nlives in the control-plane's memory.","operationId":"internal_stream_metrics_internal_stream_metrics_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveTokenPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/resolve-token":{"post":{"summary":"Internal Resolve Token","description":"Internal-only PAT resolution for the keyless MCP server. That container\ntalks to Postgres as the least-privilege mcp_role (migration 0040), which is\ndeliberately denied ALL access to personal_access_tokens -- the auth table --\nso it cannot resolve its own bearer tokens. It POSTs the token here instead;\nthis process (as relay, the legitimate owner of the token table) runs the\nexact same tokens.resolve_token used everywhere else and returns ONLY the\nowning user_id.\n\nMinimal surface by design: the token arrives in the request BODY (never a\nquery string / URL, so it can't land in an access log), and the response is\njust {\"user_id\": <uuid str> | null} -- no token_hash, name, prefix, or any\nother column of the row. Locked behind the internal-read secret\n(require_internal_secret, distinct from the relay hook secret) and the same\nCaddy @internal_only matcher, so it is never reachable from the public\ninternet.\n\nThe best-effort last_used_at bump inside resolve_token runs here now, as\nrelay, so token telemetry is preserved despite mcp_role's lack of UPDATE.\n\nBuild 1.5 (MCP OAuth): the response carries kind/scope/aud alongside\nuser_id so the resource server can enforce audience binding and tool\nscope. Additive - an older mcp-server reads user_id and ignores the\nrest, which is what makes the mcp-server-first deploy order safe.","operationId":"internal_resolve_token_internal_resolve_token_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveTokenPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/destination-status":{"post":{"summary":"Internal Destination Status","description":"Per-destination diagnostic snapshot for the CALLING token's own user,\nfor Odin (keyless MCP). Returns current per-destination live/error/idle\nstate (from destination_status, which this process can compute -- it has\nthe ffmpeg-log volume and the enforcement in-memory publish state), the\nlive input resolution, and recent drop events. Safe fields only: id /\nlabel / platform / display_name / state, plus a friendly error reason.\nNEVER a stream key, destination URL, or raw ffmpeg line.\n\n2026-07-30: user_id resolved server-side from payload.token, same as\n/internal/stream-metrics above -- see that docstring.","operationId":"internal_destination_status_internal_destination_status_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveTokenPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/plan-status":{"post":{"summary":"Internal Plan Status","description":"Plan + quota diagnostic snapshot for the CALLING token's own user, for\nOdin (keyless MCP). Reuses usage.get_quota_status + stripe_helper.PLAN_LIMITS.\nIntegers and the plan name only -- no secret, no key, no URL. hours_cap is\nnull on EVERY tier now (all plans are unlimited-hours; fair use is the\negress meter, not an hours cap). near_fairuse / over_fairuse let Odin\nnudge an upgrade for a free/standard user who is bumping the fair-use\nbandwidth ceiling; both are always False for Pro (never gated).\n\n2026-07-30: user_id resolved server-side from payload.token, same as\n/internal/stream-metrics above.","operationId":"internal_plan_status_internal_plan_status_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveTokenPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/signup":{"post":{"summary":"Api Signup","operationId":"api_signup_api_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/verify":{"get":{"summary":"Verify Email Page","description":"Render the verify-email confirmation page.\n\nDOES NOT consume the token. Email security scanners (Microsoft Safe\nLinks, Mimecast, Proofpoint, etc.) pre-fetch every URL in inbound\nmail; if this endpoint consumed the token, scanners would burn it\nbefore the real user ever clicks.\n\nThe page presents a button that POSTs to /api/verify-email, which\nis where the actual consumption happens. Real human action required.","operationId":"verify_email_page_verify_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/verify-email":{"post":{"summary":"Api Verify Email","description":"Consume the verification token. Called by the button on /verify.\n\nAccepts either JSON body {\"token\": \"...\"} or form-encoded\ntoken=... (the GET page submits via HTML form, but a JSON client\nis also useful for tests).","operationId":"api_verify_email_api_verify_email_post","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/login":{"post":{"summary":"Api Login","operationId":"api_login_api_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/login/2fa":{"post":{"summary":"Api Login 2Fa","description":"Second factor verification step.\n\nCalled after /api/login returns totp_required=True. The pending_token\nidentifies the half-completed login attempt; code is either a TOTP\ncode or a backup code (if use_backup_code=true).\n\nOn success: deletes the pending row, issues a session cookie.\nOn failure: decrements attempts_remaining. If at 0 (or already 0),\ndeletes the row, user must re-enter password.","operationId":"api_login_2fa_api_login_2fa_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Login2faBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/security.txt":{"get":{"summary":"Well Known Security Txt","description":"RFC 9116 security disclosure file.\n\nResearchers can find this at /.well-known/security.txt and use\nthe Contact address to report vulnerabilities.\n\nExpires field set to 1 year out at deploy time. Renew by\nredeploying (re-runs the timedelta calc at import time, which\nhappens on container restart).","operationId":"well_known_security_txt__well_known_security_txt_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/.well-known/assetlinks.json":{"get":{"summary":"Assetlinks Hoptest","operationId":"assetlinks_hoptest__well_known_assetlinks_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/app/auth-bridge":{"get":{"summary":"Auth Bridge","description":"App OAuth handoff. The verified App Link lands here INSIDE the\napp webview. Redeem the single-use bridge token, mint a real\nsession, set the same httponly cookie the OAuth callback uses, and\nredirect to the dashboard. The Set-Cookie lands in the webview jar\nbecause the webview navigated here.","operationId":"auth_bridge_app_auth_bridge_get","parameters":[{"name":"bt","in":"query","required":false,"schema":{"type":"string","default":"","title":"Bt"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/logout":{"post":{"summary":"Api Logout","operationId":"api_logout_api_logout_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/resend-verification":{"post":{"summary":"Api Resend Verification","description":"Resend the email verification token. Rate-limited per email:\nmax 3 sends per rolling hour. Returns 200 even when email doesn't\nexist or user is already verified -- prevents account enumeration.\nA125: IP-dim limit added on top of email-dim so an attacker can't\nenumerate emails by spreading resend attempts across many addresses.","operationId":"api_resend_verification_api_resend_verification_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendVerificationBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/forgot-password":{"post":{"summary":"Api Forgot Password","operationId":"api_forgot_password_api_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reset-password":{"post":{"summary":"Api Reset Password","operationId":"api_reset_password_api_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/odin/chat":{"post":{"summary":"Odin Chat","operationId":"odin_chat_app_odin_chat_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/odin/confirm":{"post":{"summary":"Odin Confirm","operationId":"odin_confirm_app_odin_confirm_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmBody","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/odin/history":{"get":{"summary":"Odin History","operationId":"odin_history_app_odin_history_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/odin/opened":{"post":{"summary":"Odin Opened","operationId":"odin_opened_app_odin_opened_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenedBody","default":{"source":"launcher","route":""}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/odin/rune":{"post":{"summary":"Odin Rune","operationId":"odin_rune_app_odin_rune_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuneBody","default":{"route":"","source":"panel"}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/odin/hint-seen":{"post":{"summary":"Odin Hint Seen","operationId":"odin_hint_seen_app_odin_hint_seen_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stream-info/status":{"get":{"summary":"Stream Info Status","description":"Per-platform connection + write-capability, for the form to\ndecide between the editor and a 'reconnect to enable' prompt.","operationId":"stream_info_status_api_stream_info_status_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stream-info/categories":{"get":{"summary":"Stream Info Categories","operationId":"stream_info_categories_api_stream_info_categories_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":200,"default":"","title":"Q"}},{"name":"platform","in":"query","required":false,"schema":{"type":"string","maxLength":32,"default":"twitch","title":"Platform"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stream-info/categories/resolve":{"get":{"summary":"Stream Info Categories Resolve","description":"Preview Kick's matching category for a Twitch category name, so\nthe UI can show the resolution BEFORE push, not discover it after.\nSame resolver the push path uses -- preview and push can't drift.","operationId":"stream_info_categories_resolve_api_stream_info_categories_resolve_get","parameters":[{"name":"name","in":"query","required":false,"schema":{"type":"string","maxLength":200,"default":"","title":"Name"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stream-info":{"post":{"summary":"Stream Info Push","operationId":"stream_info_push_api_stream_info_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stream-info/preset":{"get":{"summary":"Stream Info Preset Get","description":"The saved apply-on-golive preset, for the page to populate the\nform on load. {} shape with everything null/empty/False when the\nuser has never saved one -- not a 404, so the client can always\ntreat the response as \"the current saved state\".","operationId":"stream_info_preset_get_api_stream_info_preset_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Stream Info Preset Save","description":"Save (upsert) the apply-on-golive preset. This is a full\noverwrite of the saved state, not a patch -- whatever the form\nshows is what gets saved, including auto_apply. Turning auto_apply\noff through this same endpoint keeps title/category/tags (the\nrow is never deleted), matching the \"disable, don't discard\"\ncontract apply_on_golive relies on.","operationId":"stream_info_preset_save_api_stream_info_preset_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresetSaveBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/google/start":{"get":{"summary":"Oauth Google Start","description":"Redirect to Google's consent screen. State token prevents CSRF.","operationId":"oauth_google_start_api_oauth_google_start_get","parameters":[{"name":"app","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"App"}},{"name":"ref","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ref"}},{"name":"next","in":"query","required":false,"schema":{"type":"string","default":"","title":"Next"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/google/callback":{"get":{"summary":"Oauth Google Callback","description":"Exchange code for token, fetch userinfo, create/lookup session.","operationId":"oauth_google_callback_api_oauth_google_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},{"name":"__Host-oauth_nonce","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Oauth Nonce"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/google/link-notification":{"get":{"summary":"Oauth Google Link Notification","description":"Read-once: did the most recent Google sign-in auto-link to an existing\nemail/password account? Dashboard polls this once on load to know whether\nto show a notification toast.","operationId":"oauth_google_link_notification_api_oauth_google_link_notification_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/google/link-verify":{"post":{"summary":"Api Oauth Google Link Verify","description":"A15: complete the Google sign-in flow for an existing\npassword-protected Sleipnir user.\n\nFlow:\n  1. User clicks \"Sign in with Google\" on landing page.\n  2. Google OAuth callback finds an existing Sleipnir account with a\n     real password (not the Google-only sentinel).\n  3. Callback creates a LoginPending(kind='oauth_link') row and\n     redirects to /link-google?token=<pending_id> instead of\n     issuing a session.\n  4. User on /link-google enters their Sleipnir password (and TOTP\n     if 2FA is enabled).\n  5. /api/oauth/google/link-verify (this endpoint) verifies both\n     factors, atomically deletes the pending row, creates a real\n     session, sends a notification email.\n\nRate limits: DB-backed via rate_limiter module, per-IP and\nper-user_id (the latter only after pending lookup resolves).\n\nDefense-in-depth: if pending.kind != 'oauth_link' (e.g., someone\ntries to use a 2FA pending_token here), reject. The endpoints have\ndifferent attack surfaces and must not be interchangeable.","operationId":"api_oauth_google_link_verify_api_oauth_google_link_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OauthLinkVerifyBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/twitch/start":{"get":{"summary":"Oauth Twitch Start","description":"Begin the Twitch OAuth flow. Requires the user to be logged into\nSleipnir already; the returned redirect sends them to Twitch's\nconsent screen.","operationId":"oauth_twitch_start_api_oauth_twitch_start_get","parameters":[{"name":"__Host-connect_nonce","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Connect Nonce"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/twitch/callback":{"get":{"summary":"Oauth Twitch Callback","description":"Twitch redirects here with code+state. Exchange code for tokens,\nfetch the Twitch user profile, save (replacing any existing active\nconnection for this Sleipnir user), redirect to account page.\n\nShared by the chat-connect and destination-connect flows: a dest grant\nis recognised by its state matching the ISOLATED dest state store. If so,\nwe handle the destination and return; otherwise we fall through to the\nchat logic below, unchanged.","operationId":"oauth_twitch_callback_api_oauth_twitch_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},{"name":"__Host-connect_nonce","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Connect Nonce"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/twitch-dest/start":{"get":{"summary":"Oauth Twitch Dest Start","description":"Begin the Twitch DESTINATION connect flow (channel:read:stream_key).\nIsolated from the chat connect flow; same callback URL.","operationId":"oauth_twitch_dest_start_api_oauth_twitch_dest_start_get","parameters":[{"name":"next","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next"}},{"name":"__Host-connect_nonce","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Connect Nonce"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/twitch/disconnect":{"post":{"summary":"Oauth Twitch Disconnect","description":"Disconnect the user's Twitch account. Marks the active\ntwitch_connections row inactive and revokes the access token at\nTwitch (best-effort).","operationId":"oauth_twitch_disconnect_api_oauth_twitch_disconnect_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/twitch-connection":{"get":{"summary":"Me Twitch Connection","description":"Return the current user's active Twitch connection, if any.","operationId":"me_twitch_connection_api_me_twitch_connection_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/overlay-token":{"get":{"summary":"Get Overlay Token","description":"Return the user's overlay token + URL, or nulls if not minted.","operationId":"get_overlay_token_api_me_overlay_token_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Regenerate Overlay Token","description":"Mint or rotate the overlay token. The old URL dies immediately.","operationId":"regenerate_overlay_token_api_me_overlay_token_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/overlay/themes":{"get":{"summary":"Get Overlay Themes","description":"Curated chat-overlay style presets for the dashboard picker. Static style\ndata only; the overlay server re-validates the chosen params on every render\n(overlay_theme.validate_theme), so this is pure convenience, not a trust path.","operationId":"get_overlay_themes_api_overlay_themes_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/overlay/theme-generate":{"post":{"summary":"Api Generate Theme","description":"Turn a free-text style description into a VALIDATED theme param set for\nthe chat-overlay URL. The model emits only theme tokens (JSON), never\nHTML/JS, and the output is run through overlay_theme.clean_params, so nothing\nexecutable or invalid can reach the overlay. Tier-gated + burst limited.","operationId":"api_generate_theme_api_overlay_theme_generate_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/overlay/{token}/messages":{"get":{"summary":"Overlay Messages","description":"Public, token-authed chat feed for the OBS browser-source overlay.\n\nResolves the user by overlay_token_hash (same hash fn the setter uses).\nSame query and shape as /api/me/chat/messages, minus the session.\n404 on an unknown/revoked token. Intended for ~4s polling.\n\nRate limited per-IP and per-token before any DB work, so a hammered or\nleaked token URL cannot turn into unbounded query load.","operationId":"overlay_messages_api_overlay__token__messages_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/overlay/{token}/events":{"get":{"summary":"Overlay Events","description":"Public, token-authed follow/sub event feed for the alert overlay.\n\nPER-TOKEN SCOPED, read-only. The path {token} is the ONLY input that selects\na user: it is hashed and matched against User.overlay_token_hash server-side,\nand EVERY query is filtered by PlatformEvent.user_id == that user. There is\nno parameter (no user_id, no token override) by which the overlay JS could\nread another user's events; `since` only filters by time. An unknown/revoked\ntoken 404s before any event is read. Same per-IP + per-token rate limit and\ntier gate as the chat feed.\n\nInitial load (no `since`) returns an EMPTY list plus a cursor at the latest\nevent (or now), so a freshly-loaded browser source never replays old follows\nas alerts; subsequent polls pass `since` and receive only newer events.","operationId":"overlay_events_api_overlay__token__events_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/chat/messages":{"get":{"summary":"Me Chat Messages","description":"Return chat messages for the current user.\n\nWith no `since` param: last 50 messages, ascending (oldest first).\nWith `since=<ISO 8601 timestamp>`: messages with received_at > since,\nascending, limit 500.\n\nRead-only feed; polling cadence is the client's responsibility.\nSee templates/chat.html for the consumer (Step 7 v1).","operationId":"me_chat_messages_api_me_chat_messages_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/youtube-dest/start":{"post":{"summary":"Oauth Youtube Dest Start Fast","description":"Fast-path YouTube destination connect: reuse an existing active\nYoutubeConnection's tokens (refresh if near expiry) instead of a\nfresh Google round-trip, and commit the Destination change directly.\n\nPOST-only. This path writes to the DB on the caller's say-so alone\n(no Google round-trip to re-confirm consent), so it must not be\nreachable by a plain GET: SameSite=Lax still attaches the session\ncookie to a top-level cross-site GET navigation, so a GET version of\nthis fast path could be triggered by a bare link. POST closes that,\nmatching the app's mutations-aren't-GETs rule. Returns 204 when\nthere's no active connection to reuse; the caller falls back to the\nfull-OAuth GET sibling below.","operationId":"oauth_youtube_dest_start_fast_api_oauth_youtube_dest_start_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Oauth Youtube Dest Start","description":"Begin the YouTube DESTINATION connect via full OAuth (dest-mode\nstate; the shared callback saves the connection, then finalizes the\ndestination).\n\nGET-only and mutation-free by design, mirroring the Twitch/Kick dest\nroutes: it only ever redirects to Google's consent screen, never\nwrites to the DB itself. The DB-mutating fast path lives at\nPOST /api/oauth/youtube-dest/start (same path, different method) so\nit can't be triggered by a forged cross-site GET.","operationId":"oauth_youtube_dest_start_api_oauth_youtube_dest_start_get","parameters":[{"name":"next","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next"}},{"name":"__Host-connect_nonce","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Connect Nonce"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/youtube/start":{"get":{"summary":"Oauth Youtube Start","description":"Begin the YouTube OAuth flow. Requires the user to be logged into\nSleipnir already; the returned redirect sends them to Google's\nconsent screen.","operationId":"oauth_youtube_start_api_oauth_youtube_start_get","parameters":[{"name":"__Host-connect_nonce","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Connect Nonce"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/youtube/callback":{"get":{"summary":"Oauth Youtube Callback","description":"Google redirects here with code+state. Exchange code for tokens,\nfetch the YouTube channel, save (replacing any existing active\nconnection for this Sleipnir user), redirect to account page.","operationId":"oauth_youtube_callback_api_oauth_youtube_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},{"name":"__Host-connect_nonce","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Connect Nonce"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/youtube/disconnect":{"post":{"summary":"Oauth Youtube Disconnect","description":"Disconnect the user's YouTube channel: revoke every stored refresh\ntoken at Google (best-effort; revoking the refresh token also kills all\nderived access tokens), then DELETE the user's youtube_connections rows,\ntokens and channel details included. Nothing is kept: YouTube API\nServices policy III.A.2 requires deleting Authorized Data within 7 days\nof an in-app revocation, and the privacy policy says disconnecting\ndeletes it (owner decision 2026-10-19, replacing the old mark-inactive).\nA revoke that fails still deletes: the user asked to disconnect, and a\ntoken we no longer hold cannot be used by us.","operationId":"oauth_youtube_disconnect_api_oauth_youtube_disconnect_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/youtube-connection":{"get":{"summary":"Me Youtube Connection","description":"Return the current user's active YouTube connection, if any.","operationId":"me_youtube_connection_api_me_youtube_connection_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/kick-dest/start":{"get":{"summary":"Oauth Kick Dest Start","description":"Begin the Kick DESTINATION connect flow (channel:read streamkey:read,\nPKCE). Reuses the existing /api/oauth/kick/callback via an isolated dest\nstate store (Option B, mirrors Twitch).","operationId":"oauth_kick_dest_start_api_oauth_kick_dest_start_get","parameters":[{"name":"next","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next"}},{"name":"__Host-connect_nonce","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Connect Nonce"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/kick/start":{"get":{"summary":"Oauth Kick Start","description":"Begin the Kick OAuth flow. Requires the user to be logged into\nSleipnir already; the returned redirect sends them to Kick's\nconsent screen (id.kick.com).","operationId":"oauth_kick_start_api_oauth_kick_start_get","parameters":[{"name":"__Host-connect_nonce","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Connect Nonce"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/kick/callback":{"get":{"summary":"Oauth Kick Callback","description":"Kick redirects here with code+state. Exchange the code for\ntokens (PKCE), fetch the Kick user, subscribe the channel's\nchat.message.sent event to the webhook, then save the connection.\n\nThe event subscription is created BEFORE the DB save so a\nconnection row is never persisted without its subscription_id.\n\nShared with the destination-connect flow: a dest grant is recognised by\nits state matching the ISOLATED dest store. If so we handle the\ndestination and return; otherwise we fall through to the chat logic.","operationId":"oauth_kick_callback_api_oauth_kick_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},{"name":"__Host-connect_nonce","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Connect Nonce"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/oauth/kick/disconnect":{"post":{"summary":"Oauth Kick Disconnect","description":"Disconnect the user's Kick channel: delete its Kick event\nsubscriptions and revoke the token (both best-effort), then DELETE the\nuser's kick_connections rows, tokens included (owner decision\n2026-10-19, same as YouTube; replaces the old mark-inactive).","operationId":"oauth_kick_disconnect_api_oauth_kick_disconnect_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/kick-connection":{"get":{"summary":"Me Kick Connection","description":"Return the current user's active Kick connection, if any.","operationId":"me_kick_connection_api_me_kick_connection_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/destinations":{"get":{"summary":"List Destinations","operationId":"list_destinations_api_destinations_get","parameters":[{"name":"reveal","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Reveal"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Create Destination","operationId":"create_destination_api_destinations_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DestinationCreateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/destinations/{dest_id}":{"get":{"summary":"Get Destination","operationId":"get_destination_api_destinations__dest_id__get","parameters":[{"name":"dest_id","in":"path","required":true,"schema":{"type":"string","title":"Dest Id"}},{"name":"reveal","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Reveal"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"summary":"Update Destination","operationId":"update_destination_api_destinations__dest_id__patch","parameters":[{"name":"dest_id","in":"path","required":true,"schema":{"type":"string","title":"Dest Id"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DestinationUpdateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Destination","operationId":"delete_destination_api_destinations__dest_id__delete","parameters":[{"name":"dest_id","in":"path","required":true,"schema":{"type":"string","title":"Dest Id"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tokens":{"get":{"summary":"List Pat Tokens","operationId":"list_pat_tokens_api_tokens_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Create Pat Token","operationId":"create_pat_token_api_tokens_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTokenBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tokens/{token_id}":{"delete":{"summary":"Revoke Pat Token","operationId":"revoke_pat_token_api_tokens__token_id__delete","parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"string","title":"Token Id"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/dismiss-violation":{"post":{"summary":"Api Dismiss Violation","description":"Mark the user's most recent violation banner as seen.","operationId":"api_dismiss_violation_api_me_dismiss_violation_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/stream-status":{"get":{"summary":"Api Stream Status","description":"Live snapshot for the dashboard. Step 7f v2: destinations\narray always included so frontend can render dots in any state.","operationId":"api_stream_status_api_me_stream_status_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/viewers":{"get":{"summary":"Api Me Viewers","description":"Live viewer counts per connected chat platform (viewer_counts.py).\nStandard/Pro only, fetched only while the account is publishing; the page\npolls every 30 s while live and visible. Counts only, no identities.","operationId":"api_me_viewers_api_me_viewers_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/stream-summary/latest":{"get":{"summary":"Api Me Stream Summary Latest","description":"The dashboard's last-stream card (stream_summary.latest_for_card).","operationId":"api_me_stream_summary_latest_api_me_stream_summary_latest_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/usage-history":{"get":{"summary":"Api Usage History","description":"Per-day streamed hours for the user's current quota period.\nDrives the dashboard usage sparkline. Buckets by started_at and\nuses current_period_for_user() -- the same period boundary as\nthe hour-cap counter -- so the daily values sum to the same\ntotal shown as 'hours used'.","operationId":"api_usage_history_api_me_usage_history_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/regenerate-stream-key":{"post":{"summary":"Regenerate Stream Key","operationId":"regenerate_stream_key_api_me_regenerate_stream_key_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegenerateStreamKeyBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/streamerbot-token":{"get":{"summary":"Get Streamerbot Token","description":"Return the user's current Streamer.bot WS token, or null if\nthey have not generated one yet. Available to all plans for read;\nPOST (generate/regenerate) is gated by plan.","operationId":"get_streamerbot_token_api_me_streamerbot_token_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Regenerate Streamerbot Token","description":"Generate or regenerate the Streamer.bot WS token. Requires\nStandard or Pro plan. Password (and TOTP if 2FA enabled) required\nper A116 - stolen-session can't silently rotate the integration\ntoken.","operationId":"regenerate_streamerbot_token_api_me_streamerbot_token_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegenerateStreamerbotTokenBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/2fa/start":{"post":{"summary":"Api 2Fa Start","description":"Begin 2FA enrollment. Requires current password.\n\nReturns:\n    {\n      \"qr_code_data_url\": \"data:image/png;base64,...\",\n      \"secret\": \"BASE32SECRET\"   # for manual entry if QR fails\n    }\n\nIdempotent: calling repeatedly with a non-stale pending returns the\nsame QR (so refresh during enrollment doesn't break the flow).","operationId":"api_2fa_start_api_me_2fa_start_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TotpStartBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/2fa/confirm":{"post":{"summary":"Api 2Fa Confirm","description":"Confirm 2FA enrollment with a TOTP code.\n\nOn success:\n  - Promotes pending secret to active\n  - Clears pending columns\n  - Sets totp_enabled=True\n  - Generates 10 backup codes, hashes and encrypts them\n  - Returns plaintext backup codes (one-time display)\n\nExisting sessions stay alive. The user's next login will require\nthe TOTP code (handled by Layer 3 login integration).","operationId":"api_2fa_confirm_api_me_2fa_confirm_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TotpConfirmBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/2fa/disable":{"post":{"summary":"Api 2Fa Disable","description":"Disable 2FA. Requires password AND a current TOTP code (or\nbackup code). Clears all 2FA state including any pending.\n\nOn success: totp_enabled=False, all secrets/counters/backup codes\ncleared. Existing sessions stay alive. Email notification (Layer 5\nfuture) will be sent for audit trail.","operationId":"api_2fa_disable_api_me_2fa_disable_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TotpDisableBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/2fa/status":{"get":{"summary":"Api 2Fa Status","description":"Return 2FA status for the current user.\n\nReturns:\n    {\n      \"enabled\": bool,\n      \"backup_codes_remaining\": int,  # 0 if not enabled\n    }","operationId":"api_2fa_status_api_me_2fa_status_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/chat/recent":{"get":{"summary":"Get Chat Recent","description":"Return recent chat messages for HTTP-polling integrations.\n\nAuth: Bearer token or ?token= (Streamer.bot Fetch URL supports headers).\nPlan-gated: same as the Streamer.bot WS endpoint.\nA120: IP-dim rate limit protects against misconfigured Streamer.bot\npolling (or DoS via known-good token).","operationId":"get_chat_recent_api_me_chat_recent_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/me/change-password":{"post":{"summary":"Change Password","operationId":"change_password_api_me_change_password_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me":{"patch":{"summary":"Patch Me","operationId":"patch_me_api_me_patch","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfilePatchBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Account","operationId":"delete_account_api_me_delete","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAccountBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Api Me","operationId":"api_me_api_me_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/upsell-dismiss":{"post":{"summary":"Api Upsell Dismiss","description":"Persist 'don't show me this again this billing period' for a polled\nupgrade prompt (T4 bitrate / T5 fair-use egress). Upserts the current\nperiod row and sets the one flag. Idempotent; a new billing period is a\nfresh row so the nudge can return next month. Best-effort by nature -- a\nfailure just means the prompt may re-show, never a functional break.","operationId":"api_upsell_dismiss_api_me_upsell_dismiss_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsellDismissBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/checkout-session":{"post":{"summary":"Create Checkout Session","operationId":"create_checkout_session_api_billing_checkout_session_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/portal-session":{"post":{"summary":"Create Portal Session","description":"Returns the URL for Stripe's Customer Portal where users can\nmanage their subscription, update payment method, and cancel.","operationId":"create_portal_session_api_billing_portal_session_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/return":{"get":{"summary":"Billing Return","description":"Stripe redirects here after Checkout. Sync state immediately for UX;\nwebhook will also re-confirm shortly.","operationId":"billing_return_billing_return_get","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Session Id"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhook/stripe":{"post":{"summary":"Stripe Webhook","description":"Receives webhook events from Stripe. The signature header is verified\nusing STRIPE_WEBHOOK_SECRET — anyone trying to forge events without the\nsecret gets rejected.","operationId":"stripe_webhook_webhook_stripe_post","parameters":[{"name":"stripe-signature","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stripe-Signature"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhook/kick":{"post":{"summary":"Kick Webhook Endpoint","description":"Receives Kick event webhooks (chat.message.sent, etc.).\n\nVerify-and-acknowledge only: the RSA signature is verified against\nKick's public key, a valid event returns 200, an invalid one 401.\nNo database write happens here - the message-writer is a separate\nlater patch.\n\nThe raw body MUST be read before any parsing - the signature is\ncomputed over the exact raw bytes.","operationId":"kick_webhook_endpoint_webhook_kick_post","parameters":[{"name":"Kick-Event-Message-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kick-Event-Message-Id"}},{"name":"Kick-Event-Message-Timestamp","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kick-Event-Message-Timestamp"}},{"name":"Kick-Event-Signature","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kick-Event-Signature"}},{"name":"Kick-Event-Type","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kick-Event-Type"}},{"name":"Kick-Event-Subscription-Id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kick-Event-Subscription-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/config":{"get":{"summary":"Api Config","operationId":"api_config_api_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/overlays/generate":{"post":{"summary":"Api Generate Overlay","description":"HTTP entry point to the shared overlay-generation callable (the MCP\ntool reaches the SAME callable via /internal/generate-overlay). All logic\nlives in _do_generate_overlay so the two transports cannot drift.","operationId":"api_generate_overlay_api_overlays_generate_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateOverlayBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/overlays":{"get":{"summary":"Api List Overlays","description":"List the user's active overlays (with re-derived /o/ URLs) plus this\nmonth's allowance, for the dashboard.","operationId":"api_list_overlays_api_overlays_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/overlays/credits/checkout":{"post":{"summary":"Api Credits Checkout","description":"Start a one-off Stripe Checkout for overlay credits. The client sends\na preset KEY; the amount comes from overlay_credits.TOPUP_PRESETS_CENTS\nserver-side. Credit application happens in the webhook / return page /\nreconcile loop, all idempotent on the session id.","operationId":"api_credits_checkout_api_overlays_credits_checkout_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditsCheckoutBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/credits/return":{"get":{"summary":"Credits Return","description":"Stripe redirects here after a credit top-up. Apply immediately for UX\n(mirrors /billing/return's sync-now-webhook-reconfirms pattern); the\nwebhook and the reconcile loop are the other two idempotent paths.\nCredits always go to the session's metadata user_id (set server-side at\ncreation), never to the visitor, so a shared/leaked return URL cannot\nmisdirect funds.","operationId":"credits_return_billing_credits_return_get","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Session Id"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/overlays/{overlay_id}/refine":{"post":{"summary":"Api Refine Overlay","description":"HTTP entry point to the shared overlay-refine callable. All logic lives\nin _do_refine_overlay. (Refine is deliberately web-only for Odin, but the\nsame callable backs the MCP internal route for PAT clients.)","operationId":"api_refine_overlay_api_overlays__overlay_id__refine_post","parameters":[{"name":"overlay_id","in":"path","required":true,"schema":{"type":"string","title":"Overlay Id"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefineOverlayBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/generate-overlay":{"post":{"summary":"Internal Generate Overlay","description":"MCP backend for the generate_overlay tool. Runs the SAME\n_do_generate_overlay callable as the web route, so meter/ledger/spend-cap/\nburst effects are identical by construction (the parity assertion in the\ntest ladder checks exactly this).","operationId":"internal_generate_overlay_internal_generate_overlay_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalGenerateOverlayPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/refine-overlay":{"post":{"summary":"Internal Refine Overlay","description":"MCP backend for the refine_overlay tool (PAT clients only; Odin does not\nget refine). Same shared callable as the web route; ownership on the\noverlay_id is enforced inside _do_refine_overlay.","operationId":"internal_refine_overlay_internal_refine_overlay_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalRefineOverlayPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/quote/create":{"post":{"summary":"Internal Quote Create","operationId":"internal_quote_create_internal_quote_create_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalQuoteCreatePayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/quote/status":{"post":{"summary":"Internal Quote Status","operationId":"internal_quote_status_internal_quote_status_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalQuoteStatusPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/quote/refine":{"post":{"summary":"Internal Quote Refine","operationId":"internal_quote_refine_internal_quote_refine_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalQuoteRefinePayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/quote/return":{"get":{"summary":"Quote Return","description":"Public page the (not-logged-in) payer lands on after Checkout. Option-C\nbackstop to the webhook: re-fetch the session and drive the paid\ntransition. Shows no capability URL (that is the agent's, via polling).","operationId":"quote_return_quote_return_get","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Session Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/quote/cancelled":{"get":{"summary":"Quote Cancelled","operationId":"quote_cancelled_quote_cancelled_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/app/quote/claim/{quote_id}":{"get":{"summary":"Quote Claim Page","description":"Claim an agent-created overlay into an account. Logged out -> /login with\nthe audited resolve-and-compare next= guard. Logged in -> a minimal page\nwhose button POSTs to the claim API (which enforces the email match).","operationId":"quote_claim_page_app_quote_claim__quote_id__get","parameters":[{"name":"quote_id","in":"path","required":true,"schema":{"type":"string","title":"Quote Id"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/quote/{quote_id}/claim":{"post":{"summary":"Api Quote Claim","operationId":"api_quote_claim_api_quote__quote_id__claim_post","parameters":[{"name":"quote_id","in":"path","required":true,"schema":{"type":"string","title":"Quote Id"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/provision/create":{"post":{"summary":"Internal Provision Create","operationId":"internal_provision_create_internal_provision_create_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalProvisionCreatePayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/provision/status":{"post":{"summary":"Internal Provision Status","operationId":"internal_provision_status_internal_provision_status_post","parameters":[{"name":"X-Sleipnir-Hook-Auth","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Sleipnir-Hook-Auth"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalProvisionStatusPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/start/{token}":{"get":{"summary":"Start Claim Page","description":"Claim page. Consumes NOTHING (scanner-safe, like /verify): the POST\nbelow is the only consumer. Every dynamic value is escaped here (no\nautoescaping anywhere).","operationId":"start_claim_page_start__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/start/claim":{"post":{"summary":"Api Start Claim","operationId":"api_start_claim_api_start_claim_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartClaimBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/start/done":{"get":{"summary":"Start Done Page","description":"OBS-ready page. The stream key is NOT in this markup: the page fetches\nit from /api/me client-side (the same path the dashboard uses). Logged\nout -> /login with next= (the other /app pages' behaviour), so a\nbookmarked link keeps working after the session expires.","operationId":"start_done_page_app_start_done_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/start/obs-profile.zip":{"get":{"summary":"Start Obs Profile","description":"OBS profile folder (basic.ini + service.json) with the caller's OWN\nstream key embedded. Served only to the authenticated owner, no-store,\nand nothing about it is logged beyond the path (the key is never in a\nURL). Format verified against the OBS source, see provisioning.obs_profile_zip.","operationId":"start_obs_profile_app_start_obs_profile_zip_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/overlays/{overlay_id}/revoke":{"post":{"summary":"Api Revoke Overlay","description":"Soft-revoke one of the user's overlays (active=False). Owner-scoped.","operationId":"api_revoke_overlay_api_overlays__overlay_id__revoke_post","parameters":[{"name":"overlay_id","in":"path","required":true,"schema":{"type":"string","title":"Overlay Id"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/overlays/widget":{"post":{"summary":"Api Create Widget","operationId":"api_create_widget_api_overlays_widget_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/overlay/chat/{token}":{"get":{"summary":"Page Overlay Chat","description":"Public OBS chat-overlay page. No auth: the token in the path gates\nthe DATA (/api/overlay/{token}/messages), not this page. An invalid\ntoken renders an empty transparent overlay. The token is read client\nside from the URL path; this handler serves the template plus an optional\ntheme.\n\nTheme: query params (?bg=...&accent=...&font=...) are run through\noverlay_theme.validate_theme on EVERY request (so a hand-edited URL is\nvalidated exactly like a preset/AI one) and injected as a :root override.\nvalidate_theme is the security boundary: only allowlisted/clamped values\nreach the CSS, so nothing here can break out of the style block.","operationId":"page_overlay_chat_overlay_chat__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/overlay/alert/{token}":{"get":{"summary":"Page Overlay Alert","description":"Public OBS follow/sub ALERT overlay page. Same model as the chat overlay:\nno auth (the path token gates the DATA at /api/overlay/{token}/events, not\nthis page), trusted author-controlled template served same-origin on the main\nhost so there is no CSP/sandbox question, and the same validated theme\ninjection (overlay_theme.validate_theme on every request). The template reads\nthe token client side, polls the events feed, and plays an alert per new\nevent; the untrusted actor name is rendered via textContent. ?demo=1 previews\nthe look without a real event.","operationId":"page_overlay_alert_overlay_alert__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/join/{code}":{"get":{"summary":"Page Join Team","description":"Streaming Team invite landing. Read-only team lookup by invite code;\nthe join itself is a POST to /api/teams/accept from this page's button.\nInvalid/revoked codes render an identical 'not valid' state (no leak).\ndisplay_name is user-controlled -> HTML-escaped before substitution.","operationId":"page_join_team_join__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Page Landing","operationId":"page_landing__get","parameters":[{"name":"ref","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ai-overlay-generator":{"get":{"summary":"Page Ai Overlay Generator","operationId":"page_ai_overlay_generator_ai_overlay_generator_get","parameters":[{"name":"ref","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/mcp/server-card.json":{"get":{"summary":"Mcp Server Card","description":"Public MCP server card (Smithery reads this when its auto-scan cannot\ncomplete our OAuth consent). Public metadata only - tool names and\ndescriptions, no secrets. Sourced from mcp_registry/server-card.json so it\nstays single-source with the registry artifacts.","operationId":"mcp_server_card__well_known_mcp_server_card_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/faq":{"get":{"summary":"Page Faq","description":"Public FAQ (GEO/answer-engine surface): extractable Q&A + FAQPage\nJSON-LD. Served through _read_template so it carries the analytics beacon.\nCopy is tripwired (per-plan prices/limits vs PLAN_LIMITS, ld+json parse,\nstray-AI scan, and the QUOTE_FLOW_LIVE gate on the agent entry).","operationId":"page_faq_faq_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/multistream":{"get":{"summary":"Page Multistream","operationId":"page_multistream_multistream_get","parameters":[{"name":"ref","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/agents":{"get":{"summary":"Page Agents","operationId":"page_agents_agents_get","parameters":[{"name":"ref","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/multistream-bandwidth-calculator":{"get":{"summary":"Page Multistream Bandwidth Calculator","operationId":"page_multistream_bandwidth_calculator_multistream_bandwidth_calculator_get","parameters":[{"name":"ref","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sitemap.xml":{"get":{"summary":"Sitemap Xml","operationId":"sitemap_xml_sitemap_xml_get","responses":{"200":{"description":"Successful Response"}}}},"/login":{"get":{"summary":"Page Login","operationId":"page_login_login_get","parameters":[{"name":"next","in":"query","required":false,"schema":{"type":"string","default":"","title":"Next"}},{"name":"ref","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ref"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/signup":{"get":{"summary":"Page Signup","operationId":"page_signup_signup_get","parameters":[{"name":"next","in":"query","required":false,"schema":{"type":"string","default":"","title":"Next"}},{"name":"ref","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ref"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/terms":{"get":{"summary":"Page Terms","operationId":"page_terms_terms_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/changelog":{"get":{"summary":"Page Changelog","operationId":"page_changelog_changelog_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/privacy":{"get":{"summary":"Page Privacy","operationId":"page_privacy_privacy_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/hall":{"get":{"summary":"Page Hall","operationId":"page_hall_hall_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard":{"get":{"summary":"Page Dashboard Legacy","description":"Legacy URL preserved for bookmarks, Stripe-return links, and old\nsessions. Redirects to the new canonical /app/dashboard. Query strings\n(e.g. ?checkout=success from Stripe) pass through naturally because\n302 redirects preserve the query.","operationId":"page_dashboard_legacy_dashboard_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/stream-key-meta":{"get":{"summary":"Api Me Stream Key Meta","description":"Returns timestamp + days-since-rotation for the user's ingest\nstream key. Used by the dashboard to decide whether to show the\nrotation reminder banner. No sensitive data (no key contents).","operationId":"api_me_stream_key_meta_api_me_stream_key_meta_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/dashboard":{"get":{"summary":"Page App Dashboard","description":"Dashboard, with the onboarding archetype decided SERVER-SIDE so the\ncorrect block is in the initial HTML - no post-/api/me reveal, no layout\nshift (CLS fix 2026-08-22). The route has the user and the three signals are\ncheap queries; `data-onboard` drives a CSS show-rule, and the allowance line\nfor the overlay lead is rendered here too. The /api/me JS is demoted to a\nreconcile that leaves a server-decided section alone (see dashboard.html).\nOn any failure this falls back to `data-onboard=\"\"` -> the JS reveal (the\nprior behavior), never a broken page.","operationId":"page_app_dashboard_app_dashboard_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/destinations":{"get":{"summary":"Page App Destinations","operationId":"page_app_destinations_app_destinations_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/overlays":{"get":{"summary":"Page App Overlays","operationId":"page_app_overlays_app_overlays_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/stream-info":{"get":{"summary":"Page App Stream Info","operationId":"page_app_stream_info_app_stream_info_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/account":{"get":{"summary":"Page App Account","operationId":"page_app_account_app_account_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/settings/tokens":{"get":{"summary":"Page App Settings Tokens","operationId":"page_app_settings_tokens_app_settings_tokens_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/billing":{"get":{"summary":"Page App Billing","operationId":"page_app_billing_app_billing_get","parameters":[{"name":"src","in":"query","required":false,"schema":{"type":"string","default":"","title":"Src"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/help":{"get":{"summary":"Page App Help","operationId":"page_app_help_app_help_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/help/console":{"get":{"summary":"Page App Help Console","operationId":"page_app_help_console_app_help_console_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/help/streamerbot":{"get":{"summary":"Page App Help Streamerbot","operationId":"page_app_help_streamerbot_app_help_streamerbot_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/help/browser":{"get":{"summary":"Page App Help Browser","operationId":"page_app_help_browser_app_help_browser_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/help/obs":{"get":{"summary":"Page App Help Obs","operationId":"page_app_help_obs_app_help_obs_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/whip/publish":{"post":{"summary":"Api Whip Publish","description":"Authenticated WHIP signaling proxy for the browser encoder (golive-beta).\n\nThe browser never talks to the internal `whip` MediaMTX instance\ndirectly -- it POSTs its SDP offer here instead, exactly like every\nother authenticated API route (session cookie via get_current_user).\nThe stream key used for the internal WHIP publish is DERIVED from the\ncaller's own session (user.stream_key), never accepted from the\nrequest -- so a browser can only ever publish under its own logged-in\nuser's key, structurally, not by a validation check that could have a\nbug. `whip` itself has no public port; only this route can reach it.","operationId":"api_whip_publish_api_whip_publish_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/whip/event":{"post":{"summary":"Api Whip Event","operationId":"api_whip_event_api_whip_event_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhipEventBody"}}}},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/dev/report":{"post":{"summary":"Api Dev Report","operationId":"api_dev_report_api_dev_report_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/dev/webcodecs":{"get":{"summary":"Page Dev Webcodecs","description":"Throwaway WebCodecs probe (owner-only): isConfigSupported for H.264 / AAC / Opus\nplus encode timings. Standalone: touches nothing on the relay or the WHIP path.","operationId":"page_dev_webcodecs_app_dev_webcodecs_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/whip/publish/{session_id}":{"delete":{"summary":"Api Whip Publish Delete","description":"Ends a WHIP session (WHIP has no renegotiation -- reconnect means a\nfresh POST /api/whip/publish). Cross-user replay is structurally\nblocked: the internal DELETE URL is rebuilt from the CALLER's own\nstream key, so a guessed session_id under another user's key just\n404s at `whip` rather than tearing down someone else's stream.\n\nThat scoping holds only while session_id stays inside its own path\nsegment. FastAPI's `{session_id}` will not match a literal '/', but it\nDOES accept a percent-encoded one and hand it back decoded, so\n`..%2F..%2Fotherkey%2Fwhip%2F<id>` would interpolate straight past the\ncaller's own stream key and tear down another user's session. Hence the\nshape check rather than trusting the route pattern.","operationId":"api_whip_publish_delete_api_whip_publish__session_id__delete","parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/whip/status":{"get":{"summary":"Api Whip Status","description":"Is the caller's own active publish (if any) a browser WHIP session,\nlooked up server-side via the same `whip` session list `_whip_kick_session`\nreads. Lets /app/golive-beta tell \"you're live from this browser, a\nprevious tab\" apart from \"you're live from a real external encoder\"\nafter a fresh page load, where nothing client-side remembers which.","operationId":"api_whip_status_api_whip_status_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/whip/stop":{"post":{"summary":"Api Whip Stop","description":"Stop the caller's own active browser WHIP session, found server-side\nrather than by a session id the client must have remembered. Without\nthis, a page reload mid-stream leaves no way to stop a browser-originated\nstream at all: /api/whip/publish's collision check refuses a fresh\npublish attempt outright (relay truth says already live), and the only\nother caller of _whip_kick_session is the orphan reap inside that same\nroute, which is gated on the session NOT actually reaching the relay --\nthe opposite of this case.","operationId":"api_whip_stop_api_whip_stop_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/golive-beta":{"get":{"summary":"Page App Golive Beta","description":"WHIP browser-encoder test slice. Crude, internal-only test page (not\nlinked from nav): composites webcam+screen, forces H.264, WHIP-publishes\nto the dedicated `whip` MediaMTX instance (docker-compose.yml), which\nrepublishes as ordinary RTMP to the production relay on the user's real\ningest key. Reads the key from the existing /api/me response, same as\nthe dashboard's connection card.","operationId":"page_app_golive_beta_app_golive_beta_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/help/troubleshooting":{"get":{"summary":"Page App Help Troubleshooting","operationId":"page_app_help_troubleshooting_app_help_troubleshooting_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/help/overlay":{"get":{"summary":"Page App Help Overlay","operationId":"page_app_help_overlay_app_help_overlay_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/help/destinations":{"get":{"summary":"Page App Help Destinations","operationId":"page_app_help_destinations_app_help_destinations_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/team":{"get":{"summary":"Page App Team","operationId":"page_app_team_app_team_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app/chat":{"get":{"summary":"Page App Chat","description":"Retired 2026-07-26: combined chat now lives on the Live page, which shows\nthe same feed whether the video comes from the browser encoder or the user's\nown encoder.\n\nA 302 rather than a deletion, because bookmarks and already-sent emails\nshould land somewhere sensible instead of a 404. Kept as a redirect rather\nthan left serving chat.html: two live chat surfaces would drift, and that is\nexactly the failure this codebase keeps paying for (the emote-bracket bug\nbetween two chat renderers, the same platform logo wrong in six places).\nOne live chat surface, not two.\n\ntemplates/chat.html is deliberately left in the tree, migrated and working,\nso reverting is a one-line change here rather than a rebuild.","operationId":"page_app_chat_app_chat_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/forgot-password":{"get":{"summary":"Page Forgot Password","operationId":"page_forgot_password_forgot_password_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reset-password":{"get":{"summary":"Page Reset Password","operationId":"page_reset_password_reset_password_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/link-google":{"get":{"summary":"Link Google Page","description":"A15: render the password (+ TOTP) confirmation page.\n\nReached via OAuth-callback redirect for password-protected users.\nReads the LoginPending row to verify the token is live AND look up\nthe user (so the rendered form knows whether to show the TOTP\nfield). Pure read - no state mutation on GET (matches /verify GET\npattern; mutation happens only on the POST verify endpoint).","operationId":"link_google_page_link_google_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/team/{name}":{"get":{"summary":"Page Public Team Board","description":"PUBLIC live board page — no auth. Since 2026-09-23 the main content\n(title, team name, live line, bio, member rows with live state and\nchannel links) is SERVER-RENDERED into the HTML, so crawlers and link\nunfurlers see the team rather than \"Loading\" plus the not-found copy;\nthe JS keeps refreshing the same content from\n/api/teams/public/<name> every 60s for live updates. An unknown or\ninvalid slug renders the not-found state with a real 404 (the API\nalready 404s). Slug is validated before substitution so nothing unsafe\nreaches the template; every SSR value is escaped in\n_render_team_public_ssr. OG/Twitter tags are injected server-side\nbecause crawlers (Discord, Twitter, Slack) do not execute JS.","operationId":"page_public_team_board_team__name__get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/teams/public/{name}":{"get":{"summary":"Api Public Team Board","operationId":"api_public_team_board_api_teams_public__name__get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/teams/nickname":{"post":{"summary":"Api Team Nickname","operationId":"api_team_nickname_api_teams_nickname_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_TeamNickBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/teams/leave":{"post":{"summary":"Api Team Leave","operationId":"api_team_leave_api_teams_leave_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/teams/disband":{"post":{"summary":"Api Team Disband","operationId":"api_team_disband_api_teams_disband_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_TeamDisbandBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/teams":{"get":{"summary":"Page Teams Board","description":"PUBLIC discover board — no auth.","operationId":"page_teams_board_teams_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/teams/board":{"get":{"summary":"Api Teams Board","operationId":"api_teams_board_api_teams_board_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/teams/banner":{"post":{"summary":"Api Team Banner Upload","operationId":"api_team_banner_upload_api_teams_banner_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_api_team_banner_upload_api_teams_banner_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Api Team Banner Delete","operationId":"api_team_banner_delete_api_teams_banner_delete","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/teams/branding":{"post":{"summary":"Api Team Branding","operationId":"api_team_branding_api_teams_branding_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_TeamBrandingBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/teams/{name}/follow":{"post":{"summary":"Api Team Follow","operationId":"api_team_follow_api_teams__name__follow_post","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Api Team Unfollow","operationId":"api_team_unfollow_api_teams__name__follow_delete","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}},{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/teams/{name}/here":{"post":{"summary":"Api Team Here","description":"Presence heartbeat from the public team page. Deliberately\nnever errors outward — presence is best-effort decoration.","operationId":"api_team_here_api_teams__name__here_post","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/teams":{"post":{"summary":"Create Team","operationId":"create_team_api_teams_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamCreateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/teams/invite":{"post":{"summary":"Rotate Team Invite","description":"Mint/rotate the owner's team invite code. Owner-only.","operationId":"rotate_team_invite_api_teams_invite_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Revoke Team Invite","description":"Revoke the owner's invite code (SET NULL). Owner-only.","operationId":"revoke_team_invite_api_teams_invite_delete","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/teams/accept":{"post":{"summary":"Accept Team Invite","description":"Redeem an invite code. Race-safe via SELECT FOR UPDATE on the team.","operationId":"accept_team_invite_api_teams_accept_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamJoinBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/team":{"get":{"summary":"Get My Team","operationId":"get_my_team_api_me_team_get","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/push/token":{"post":{"summary":"Api Push Token","description":"Register/refresh an FCM device token for the current user.\nUpsert: same token re-points to whoever is currently logged in on\nthat device and bumps last_seen_at. FCM tokens rotate, so the client\nmay POST repeatedly — this is idempotent.","operationId":"api_push_token_api_push_token_post","parameters":[{"name":"relay_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relay Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushTokenBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AuthPayload":{"properties":{"user":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"User","default":""},"password":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Password","default":""},"ip":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Ip","default":""},"action":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Action","default":""},"path":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Path","default":""},"protocol":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Protocol","default":""},"id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Id","default":""},"query":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Query","default":""}},"type":"object","title":"AuthPayload"},"Body_api_team_banner_upload_api_teams_banner_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_api_team_banner_upload_api_teams_banner_post"},"CategorySelection":{"properties":{"id":{"type":"string","maxLength":200,"title":"Id"},"name":{"type":"string","maxLength":200,"title":"Name","default":""}},"type":"object","required":["id"],"title":"CategorySelection"},"ChangePasswordBody":{"properties":{"current_password":{"type":"string","maxLength":200,"title":"Current Password"},"new_password":{"type":"string","maxLength":200,"minLength":10,"title":"New Password"},"code":{"type":"string","title":"Code","default":""},"use_backup_code":{"type":"boolean","title":"Use Backup Code","default":false}},"type":"object","required":["current_password","new_password"],"title":"ChangePasswordBody"},"ChatBody":{"properties":{"text":{"type":"string","maxLength":2000,"minLength":1,"title":"Text"},"route":{"type":"string","maxLength":200,"title":"Route","default":""}},"type":"object","required":["text"],"title":"ChatBody"},"CheckoutBody":{"properties":{"price_id":{"type":"string","maxLength":200,"minLength":10,"title":"Price Id"},"source":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Source"}},"type":"object","required":["price_id"],"title":"CheckoutBody"},"ConfirmBody":{"properties":{"call_id":{"type":"string","maxLength":200,"title":"Call Id"}},"type":"object","title":"ConfirmBody"},"CreateTokenBody":{"properties":{"name":{"type":"string","maxLength":80,"minLength":1,"title":"Name"}},"type":"object","required":["name"],"title":"CreateTokenBody"},"CreditsCheckoutBody":{"properties":{"preset":{"type":"string","title":"Preset"}},"type":"object","required":["preset"],"title":"CreditsCheckoutBody"},"DeleteAccountBody":{"properties":{"confirmation":{"type":"string","maxLength":20,"title":"Confirmation"},"password":{"type":"string","maxLength":200,"title":"Password"},"code":{"type":"string","title":"Code","default":""},"use_backup_code":{"type":"boolean","title":"Use Backup Code","default":false}},"type":"object","required":["confirmation","password"],"title":"DeleteAccountBody"},"DestinationCreateBody":{"properties":{"label":{"type":"string","maxLength":40,"minLength":1,"title":"Label"},"url":{"type":"string","maxLength":500,"minLength":10,"title":"Url"},"platform":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Platform"}},"type":"object","required":["label","url"],"title":"DestinationCreateBody"},"DestinationUpdateBody":{"properties":{"label":{"anyOf":[{"type":"string","maxLength":40,"minLength":1},{"type":"null"}],"title":"Label"},"url":{"anyOf":[{"type":"string","maxLength":500,"minLength":10},{"type":"null"}],"title":"Url"},"enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled"}},"type":"object","title":"DestinationUpdateBody"},"FanoutFailedPayload":{"properties":{"path":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Path","default":""},"reason":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Reason","default":""},"attempt":{"anyOf":[{"type":"integer","maximum":1000000.0,"minimum":0.0},{"type":"null"}],"title":"Attempt","default":0}},"type":"object","title":"FanoutFailedPayload"},"FanoutLegDiedPayload":{"properties":{"path":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Path","default":""},"index":{"anyOf":[{"type":"integer","maximum":64.0,"minimum":0.0},{"type":"null"}],"title":"Index","default":0},"dest_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Dest Id","default":""},"attempt":{"anyOf":[{"type":"integer","maximum":1000000.0,"minimum":0.0},{"type":"null"}],"title":"Attempt","default":0},"ran_seconds":{"anyOf":[{"type":"integer","maximum":10000000.0,"minimum":0.0},{"type":"null"}],"title":"Ran Seconds","default":0},"exit_code":{"anyOf":[{"type":"integer","maximum":1024.0,"minimum":-1024.0},{"type":"null"}],"title":"Exit Code"},"gave_up":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Gave Up","default":false}},"type":"object","title":"FanoutLegDiedPayload"},"ForgotPasswordBody":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"ForgotPasswordBody"},"GenerateOverlayBody":{"properties":{"prompt":{"type":"string","title":"Prompt"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","required":["prompt"],"title":"GenerateOverlayBody"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InternalGenerateOverlayPayload":{"properties":{"token":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Token","default":""},"prompt":{"anyOf":[{"type":"string","maxLength":2200},{"type":"null"}],"title":"Prompt","default":""},"name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Name"}},"type":"object","title":"InternalGenerateOverlayPayload"},"InternalProvisionCreatePayload":{"properties":{"email":{"type":"string","maxLength":320,"title":"Email"},"destinations":{"items":{},"type":"array","title":"Destinations"},"team_name":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Team Name"},"agent_name":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Agent Name"},"ip":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Ip"}},"type":"object","required":["email"],"title":"InternalProvisionCreatePayload"},"InternalProvisionStatusPayload":{"properties":{"provision_id":{"type":"string","maxLength":64,"title":"Provision Id"}},"type":"object","required":["provision_id"],"title":"InternalProvisionStatusPayload"},"InternalQuoteCreatePayload":{"properties":{"prompt":{"type":"string","maxLength":4000,"title":"Prompt","default":""},"agent_name":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Agent Name"},"ip":{"type":"string","maxLength":64,"title":"Ip","default":""}},"type":"object","title":"InternalQuoteCreatePayload"},"InternalQuoteRefinePayload":{"properties":{"quote_id":{"type":"string","maxLength":64,"title":"Quote Id","default":""},"note":{"type":"string","maxLength":4000,"title":"Note","default":""},"ip":{"type":"string","maxLength":64,"title":"Ip","default":""}},"type":"object","title":"InternalQuoteRefinePayload"},"InternalQuoteStatusPayload":{"properties":{"quote_id":{"type":"string","maxLength":64,"title":"Quote Id","default":""}},"type":"object","title":"InternalQuoteStatusPayload"},"InternalRefineOverlayPayload":{"properties":{"token":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Token","default":""},"overlay_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Overlay Id","default":""},"note":{"anyOf":[{"type":"string","maxLength":2200},{"type":"null"}],"title":"Note","default":""}},"type":"object","title":"InternalRefineOverlayPayload"},"Login2faBody":{"properties":{"pending_token":{"type":"string","title":"Pending Token"},"code":{"type":"string","title":"Code"},"use_backup_code":{"type":"boolean","title":"Use Backup Code","default":false}},"type":"object","required":["pending_token","code"],"title":"Login2faBody"},"LoginBody":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","maxLength":200,"title":"Password"}},"type":"object","required":["email","password"],"title":"LoginBody"},"OauthLinkVerifyBody":{"properties":{"pending_token":{"type":"string","title":"Pending Token"},"password":{"type":"string","maxLength":200,"title":"Password"},"code":{"type":"string","title":"Code","default":""},"use_backup_code":{"type":"boolean","title":"Use Backup Code","default":false}},"type":"object","required":["pending_token","password"],"title":"OauthLinkVerifyBody","description":"A15: body for /api/oauth/google/link-verify.\n\npending_token comes from the oauth_link LoginPending row created by\nthe Google OAuth callback. password is the user's Sleipnir password.\nIf the user has 2FA enabled, code (and optionally use_backup_code)\nare also required."},"OpenedBody":{"properties":{"source":{"type":"string","maxLength":16,"title":"Source","default":"launcher"},"route":{"type":"string","maxLength":200,"title":"Route","default":""}},"type":"object","title":"OpenedBody"},"PresetSaveBody":{"properties":{"title":{"anyOf":[{"type":"string","maxLength":140},{"type":"null"}],"title":"Title"},"category":{"anyOf":[{"$ref":"#/components/schemas/CategorySelection"},{"type":"null"}]},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags"},"auto_apply":{"type":"boolean","title":"Auto Apply","default":false}},"type":"object","title":"PresetSaveBody"},"ProfilePatchBody":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":80,"minLength":1},{"type":"null"}],"title":"Name"},"public_listing_optout":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Public Listing Optout"},"stream_summary_email":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stream Summary Email"},"share_credit_optin":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Share Credit Optin"}},"type":"object","title":"ProfilePatchBody"},"PushBody":{"properties":{"title":{"anyOf":[{"type":"string","maxLength":140},{"type":"null"}],"title":"Title"},"category":{"anyOf":[{"$ref":"#/components/schemas/CategorySelection"},{"type":"null"}]},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags"},"platforms":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Platforms"}},"type":"object","title":"PushBody"},"PushTokenBody":{"properties":{"token":{"type":"string","title":"Token"},"platform":{"type":"string","title":"Platform","default":"android"}},"type":"object","required":["token"],"title":"PushTokenBody"},"RefineOverlayBody":{"properties":{"note":{"type":"string","title":"Note"}},"type":"object","required":["note"],"title":"RefineOverlayBody"},"RegenerateStreamKeyBody":{"properties":{"password":{"type":"string","maxLength":200,"title":"Password"},"code":{"type":"string","title":"Code","default":""},"use_backup_code":{"type":"boolean","title":"Use Backup Code","default":false}},"type":"object","required":["password"],"title":"RegenerateStreamKeyBody","description":"A115: password required, TOTP required if 2FA enabled."},"RegenerateStreamerbotTokenBody":{"properties":{"password":{"type":"string","maxLength":200,"title":"Password"},"code":{"type":"string","title":"Code","default":""},"use_backup_code":{"type":"boolean","title":"Use Backup Code","default":false}},"type":"object","required":["password"],"title":"RegenerateStreamerbotTokenBody","description":"A116: password required, TOTP required if 2FA enabled."},"ResendVerificationBody":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"ResendVerificationBody"},"ResetPasswordBody":{"properties":{"token":{"type":"string","maxLength":128,"title":"Token"},"new_password":{"type":"string","maxLength":200,"minLength":10,"title":"New Password"}},"type":"object","required":["token","new_password"],"title":"ResetPasswordBody"},"ResolveTokenPayload":{"properties":{"token":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Token","default":""}},"type":"object","title":"ResolveTokenPayload"},"RuneBody":{"properties":{"route":{"type":"string","maxLength":200,"title":"Route","default":""},"source":{"type":"string","maxLength":16,"title":"Source","default":"panel"},"tz":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Tz"},"tz_offset":{"anyOf":[{"type":"integer","maximum":900.0,"minimum":-900.0},{"type":"null"}],"title":"Tz Offset"}},"type":"object","title":"RuneBody"},"SignupBody":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","maxLength":200,"minLength":10,"title":"Password"},"name":{"type":"string","maxLength":80,"minLength":1,"title":"Name"},"ref":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Ref"},"next":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Next"}},"type":"object","required":["email","password","name"],"title":"SignupBody"},"StartClaimBody":{"properties":{"token":{"type":"string","maxLength":128,"minLength":16,"title":"Token"},"password":{"type":"string","maxLength":200,"minLength":10,"title":"Password"},"name":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Name"}},"type":"object","required":["token","password"],"title":"StartClaimBody"},"TeamCreateBody":{"properties":{"display_name":{"type":"string","title":"Display Name"},"name":{"type":"string","title":"Name"}},"type":"object","required":["display_name","name"],"title":"TeamCreateBody"},"TeamJoinBody":{"properties":{"code":{"type":"string","title":"Code"}},"type":"object","required":["code"],"title":"TeamJoinBody"},"TotpConfirmBody":{"properties":{"code":{"type":"string","title":"Code"}},"type":"object","required":["code"],"title":"TotpConfirmBody"},"TotpDisableBody":{"properties":{"password":{"type":"string","maxLength":200,"title":"Password"},"code":{"type":"string","title":"Code"},"use_backup_code":{"type":"boolean","title":"Use Backup Code","default":false}},"type":"object","required":["password","code"],"title":"TotpDisableBody"},"TotpStartBody":{"properties":{"password":{"type":"string","maxLength":200,"title":"Password"}},"type":"object","required":["password"],"title":"TotpStartBody"},"UpsellDismissBody":{"properties":{"kind":{"type":"string","pattern":"^(bitrate|egress)$","title":"Kind"}},"type":"object","required":["kind"],"title":"UpsellDismissBody"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WhipEventBody":{"properties":{"event":{"type":"string","maxLength":16,"title":"Event"},"reason":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Reason"}},"type":"object","required":["event"],"title":"WhipEventBody"},"_TeamBrandingBody":{"properties":{"bio":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bio"},"accent_color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accent Color"}},"type":"object","title":"_TeamBrandingBody"},"_TeamDisbandBody":{"properties":{"confirm_name":{"type":"string","title":"Confirm Name","default":""}},"type":"object","title":"_TeamDisbandBody"},"_TeamNickBody":{"properties":{"nickname":{"type":"string","title":"Nickname","default":""}},"type":"object","title":"_TeamNickBody"}}}}