{"openapi":"3.1.0","info":{"title":"Tandem API","description":"Communication fabric for human+Claude pairs","version":"0.0.1"},"paths":{"/v1/admin/handles":{"get":{"tags":["admin"],"summary":"Admin List Handles","description":"List every handle in the system (admin only, read-only).\n\nDiagnostic added 2026-06-06: the tandem.cc -> tandem-cc.com cutover\nsurfaced that real handles still live under the OLD @tandem.cc domain\n(the gated migration #7 that renames them hasn't run). This lets us\neyeball exactly which local parts are taken and under which domain, so\nwe know the full scope the availability fix (_handle_taken) now covers.\n\nGrouped/ordered by domain then local part. Returns no tokens or secrets\n— just the public-ish handle id, display name, signed_by, is_tandem,\ncreated_at.","operationId":"admin_list_handles_v1_admin_handles_get","parameters":[{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin List Handles V1 Admin Handles Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["admin"],"summary":"Admin Upsert Handle","description":"Create or update a handle row.\n\nBody:\n  {\n    \"handle\": \"jack@tandem-cc.com\",             # required\n    \"display_name\": \"Jack\",                  # optional\n    \"signed_by\": \"jack\",                     # optional (attribution short form)\n    \"is_tandem\": true                        # optional (default true when\n                                             #           we're provisioning\n                                             #           internal handles)\n  }","operationId":"admin_upsert_handle_v1_admin_handles_post","parameters":[{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Upsert Handle V1 Admin Handles Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/handles/{handle_id}/reseed-welcome":{"post":{"tags":["admin"],"summary":"Admin Reseed Welcome","description":"Wipe an existing handle's welcome thread(s) and re-seed with the\ncurrent template.\n\nBrian m_b4372290-follow-up (Leslie session 2026-04-25). The welcome\ntemplate was retired mid-session — Leslie had already received the\nold chatty version. This endpoint backfills: deletes any existing\nwelcome thread(s) that the well-known welcome address sent to this\nhandle, then runs the seed function again so the user sees the new\nminimal welcome.\n\nWhy the matcher is conservative: only deletes threads where\nfrom_handle is the known welcome sender AND the subject matches\na known welcome phrase (current OR previous). This intentionally\ndoesn't cascade to \"any thread between Brian and the user\" because\nwe don't want a future bug to wipe real conversation history.\n\nPath:\n    POST /v1/admin/handles/<handle>/reseed-welcome\n\nAuth: admin API key.\n\nReturns:\n    {\"ok\": true, \"handle\": \"leslie@tandem-cc.com\", \"deleted_threads\": N,\n     \"reseeded\": true}","operationId":"admin_reseed_welcome_v1_admin_handles__handle_id__reseed_welcome_post","parameters":[{"name":"handle_id","in":"path","required":true,"schema":{"type":"string","title":"Handle Id"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Reseed Welcome V1 Admin Handles  Handle Id  Reseed Welcome Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/threads/{thread_id}/participants":{"post":{"tags":["admin"],"summary":"Admin Add Thread Participant","description":"Add a handle as a participant to an existing thread.\n\nUse case: surface a thread in someone's inbox without rewriting the\nmessage history. The original messages stay immutable; the new\nparticipant gains read/reply visibility going forward. Brian m_…\n2026-04-30 — adding himself to threads Leslie originated to Joe so\nhe can engage without tampering with the audit trail (Concept 1\nintegrity preserved).\n\nBody:\n  {\n    \"handle\": \"brian@tandem-cc.com\",        # required\n    \"role\":   \"participant\",            # optional, default \"participant\"\n    \"added_by\": \"brian@tandem-cc.com\"       # optional, defaults to handle (self-add)\n  }\n\nIdempotent — ON CONFLICT DO NOTHING.","operationId":"admin_add_thread_participant_v1_admin_threads__thread_id__participants_post","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"string","title":"Thread Id"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Add Thread Participant V1 Admin Threads  Thread Id  Participants Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/stats":{"get":{"tags":["admin"],"summary":"Admin Stats","description":"Return a rollup of preview-launch activity for the admin panel.\n\n``recent_limit`` (1–50) caps how many recent signups + reservations\nto include in the response — keeps payload small on busy days.","operationId":"admin_stats_v1_admin_stats_get","parameters":[{"name":"recent_limit","in":"query","required":false,"schema":{"type":"integer","default":10,"title":"Recent Limit"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Stats V1 Admin Stats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/notifications":{"post":{"tags":["admin"],"summary":"Admin Upsert Notification","description":"Create or update a notification channel for a handle.\n\nBody:\n  {\n    \"handle\":  \"brian@tandem-cc.com\",   # required\n    \"kind\":    \"email\",              # required: email | slack | sms | webhook\n    \"target\":  \"brian@acme.com\",     # required; shape depends on kind\n    \"enabled\": true                  # optional (default true)\n  }\n\nIdempotent on (handle, kind, target): re-posting flips `enabled`\nif different.","operationId":"admin_upsert_notification_v1_admin_notifications_post","parameters":[{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Upsert Notification V1 Admin Notifications Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["admin"],"summary":"Admin List Notifications","description":"List notification channels for a specific handle.\n\n``?handle=`` is required (we don't expose a full cross-user dump\nfrom this endpoint — admin_stats is for observability overviews).","operationId":"admin_list_notifications_v1_admin_notifications_get","parameters":[{"name":"handle","in":"query","required":false,"schema":{"type":"string","default":"","title":"Handle"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin List Notifications V1 Admin Notifications Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/notifications/{channel_id}":{"delete":{"tags":["admin"],"summary":"Admin Delete Notification","description":"Delete a notification channel. ``?handle=`` is required as a\nsafety interlock so a typo-ed ``channel_id`` can't accidentally\nwipe a different user's channel.","operationId":"admin_delete_notification_v1_admin_notifications__channel_id__delete","parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"integer","title":"Channel Id"}},{"name":"handle","in":"query","required":false,"schema":{"type":"string","default":"","title":"Handle"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Delete Notification V1 Admin Notifications  Channel Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/tokens":{"post":{"tags":["admin"],"summary":"Admin Issue Token","description":"Issue a new bearer token for a handle.\n\nBody:\n  {\n    \"handle\": \"jack@tandem-cc.com\",   # required, must exist\n    \"name\": \"claude-web\"          # required, unique per handle among active tokens\n  }\n\nThe raw token is returned in this response ONCE and never retrievable\nagain. Caller is responsible for handing it to the user.","operationId":"admin_issue_token_v1_admin_tokens_post","parameters":[{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Issue Token V1 Admin Tokens Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["admin"],"summary":"Admin List Tokens","description":"List all tokens (active + revoked) for a handle. Returns metadata\nonly — no raw values, no hashes.","operationId":"admin_list_tokens_v1_admin_tokens_get","parameters":[{"name":"handle","in":"query","required":true,"schema":{"type":"string","title":"Handle"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin List Tokens V1 Admin Tokens Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/tokens/revoke":{"post":{"tags":["admin"],"summary":"Admin Revoke Token","description":"Revoke the active token with the given (handle, name).\n\nBody:\n  {\n    \"handle\": \"jack@tandem-cc.com\",\n    \"name\": \"claude-web\"\n  }","operationId":"admin_revoke_token_v1_admin_tokens_revoke_post","parameters":[{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Admin Revoke Token V1 Admin Tokens Revoke Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/drafts":{"post":{"tags":["drafts"],"summary":"Create Draft","description":"Create a server-side draft.\n\nBody:\n  {\n    \"from_handle\": \"brian@tandem-cc.com\",     # required\n    \"to\": [\"joe@tandem-cc.com\"],              # required, >=1 recipient\n    \"body\": \"...\",                        # required (may be empty string)\n    \"subject\": \"...\",                     # optional\n    \"cc\": [\"...\"],                        # optional\n    \"in_reply_to\": \"m_...\",               # optional; resolves thread_id\n    \"drafted_by\": \"claude\" | \"human\"      # optional; default \"claude\"\n  }\n\nReturns the created draft including its draft_id.","operationId":"create_draft_v1_drafts_post","parameters":[{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}},{"name":"Authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Draft V1 Drafts Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/drafts/{draft_id}":{"get":{"tags":["drafts"],"summary":"Get Draft","operationId":"get_draft_v1_drafts__draft_id__get","parameters":[{"name":"draft_id","in":"path","required":true,"schema":{"type":"string","title":"Draft Id"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}},{"name":"Authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Draft V1 Drafts  Draft Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/drafts/{draft_id}/send":{"post":{"tags":["drafts"],"summary":"Send Draft","description":"Convert a draft to a sent message via the API's transport.\n\nThe draft's from_handle becomes the message's from_handle. The\n`signed_by` attribution defaults to the from_handle's prefix unless\noverridden in the request body. This is the point where the human's\napproval is formally recorded — the MCP client calls this only after\nthe user clicks 'send' in Claude's UI.\n\nOptional body:\n  {\n    \"signed_by\": \"brian\"   # short-form attribution override\n  }","operationId":"send_draft_v1_drafts__draft_id__send_post","parameters":[{"name":"draft_id","in":"path","required":true,"schema":{"type":"string","title":"Draft Id"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}},{"name":"Authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Send Draft V1 Drafts  Draft Id  Send Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/launch-signups":{"post":{"tags":["public"],"summary":"Launch Signups","operationId":"launch_signups_launch_signups_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["public"],"summary":"Launch Signups Preflight","description":"CORS preflight for the public signup form.","operationId":"launch_signups_preflight_launch_signups_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/signup/check":{"post":{"tags":["public"],"summary":"Signup Check","description":"Pre-flight: is ``handle`` valid and available?\n\nThe signup page calls this as the user types. Same shape as\n``/v1/handles/check`` but gated on the page password instead of a\nmagic-link token. Does NOT claim the handle.\n\nBody:\n    {\"password\": \"...\", \"handle\": \"leslie\"}\n\nReturns:\n    {\"available\": true,  \"handle\": \"leslie\", \"full\": \"leslie@tandem-cc.com\"}\n    — or —\n    {\"available\": false, \"handle\": \"leslie\", \"suggestions\": [\"leslie2\"]}","operationId":"signup_check_signup_check_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Signup Check Signup Check Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/signup/claim":{"post":{"tags":["public"],"summary":"Signup Claim","description":"Claim a handle and mint the user's first bearer token.\n\nPast the password gate, this endpoint is the moral equivalent of:\n\n    POST /v1/admin/handles      (create the handle)\n    POST /v1/admin/tokens       (mint a bearer token)\n    + welcome-thread seed       (so first inbox isn't empty)\n\nMirrors the result of ``/v1/handles/reserve`` — same handle row,\nsame token row, same welcome-thread seeding — just gated on the\npage password instead of a magic-link reservation.\n\nReturns the raw bearer token in the body. The token is shown\nONCE; if the user loses it an admin must revoke and reissue.\n\nBody:\n    {\n      \"password\":     \"...\",\n      \"handle\":       \"leslie\",\n      \"display_name\": \"Leslie\"        # optional, defaults to handle\n    }\n\nReturns:\n    {\n      \"ok\": true,\n      \"handle\": \"leslie@tandem-cc.com\",\n      \"token\":  \"tandem_...\",\n      \"snippets\": {\n        \"desktop\": { \"mcpServers\": { ... } },\n        \"code\":    { \"mcpServers\": { ... } }\n      },\n      \"warning\": \"This token is shown ONCE — copy it now ...\"\n    }","operationId":"signup_claim_signup_claim_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Signup Claim Signup Claim Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/signup/open-check":{"post":{"tags":["public"],"summary":"Signup Open Check","description":"Live handle-availability for the open (no-password) signup page.","operationId":"signup_open_check_signup_open_check_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Signup Open Check Signup Open Check Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/signup/open":{"post":{"tags":["public"],"summary":"Signup Open","description":"Open public signup: claim a handle, mint a token, AND set a login\nsession cookie so the user is signed in immediately. No password gate.","operationId":"signup_open_signup_open_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Signup Open Signup Open Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/mcp-registry-auth":{"get":{"tags":["oauth"],"summary":"Mcp Registry Auth","description":"Serve the MCP Registry HTTP-auth proof string (env-configured).","operationId":"mcp_registry_auth__well_known_mcp_registry_auth_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"tags":["oauth"],"summary":"Protected Resource Metadata","description":"RFC 9728 Protected Resource Metadata for our /mcp endpoint.","operationId":"protected_resource_metadata__well_known_oauth_protected_resource_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Protected Resource Metadata  Well Known Oauth Protected Resource Get"}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"tags":["oauth"],"summary":"Authorization Server Metadata","description":"RFC 8414 Authorization Server Metadata.","operationId":"authorization_server_metadata__well_known_oauth_authorization_server_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Authorization Server Metadata  Well Known Oauth Authorization Server Get"}}}}}}},"/oauth/register":{"post":{"tags":["oauth"],"summary":"Register Client","description":"RFC 7591 Dynamic Client Registration.\n\nAccepts a JSON body with at least `redirect_uris` and `client_name`.\nReturns a generated client_id plus the registered metadata.","operationId":"register_client_oauth_register_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Register Client Oauth Register Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/authorize":{"get":{"tags":["oauth"],"summary":"Authorize","description":"RFC 6749 §4.1.1 + OAuth 2.1 §1.3.1 + RFC 7636 + RFC 8707.","operationId":"authorize_oauth_authorize_get","parameters":[{"name":"response_type","in":"query","required":true,"schema":{"type":"string","title":"Response Type"}},{"name":"client_id","in":"query","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string","title":"Redirect Uri"}},{"name":"code_challenge","in":"query","required":true,"schema":{"type":"string","title":"Code Challenge"}},{"name":"code_challenge_method","in":"query","required":true,"schema":{"type":"string","title":"Code Challenge Method"}},{"name":"resource","in":"query","required":true,"schema":{"type":"string","title":"Resource"}},{"name":"scope","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/consent":{"post":{"tags":["oauth"],"summary":"Consent","operationId":"consent_oauth_consent_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Consent Oauth Consent Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/token":{"post":{"tags":["oauth"],"summary":"Token","operationId":"token_oauth_token_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_token_oauth_token_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Token Oauth Token Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/login":{"post":{"tags":["oauth"],"summary":"Login","description":"Create a session cookie from a valid bearer token.\n\nBody:\n  {\n    \"token\": \"tandem_...\"           # existing bearer (admin-issued\n                                    # OR OAuth-issued — both work)\n  }\n\nSets a HttpOnly Secure SameSite=Lax cookie on success. Returns\n{\"ok\": true, \"handle\": \"...\"}; the page is responsible for\nnavigating to wherever ?next= said to go (consent UI by default).","operationId":"login_login_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/logout":{"post":{"tags":["oauth"],"summary":"Logout","description":"Revoke the calling session cookie. Idempotent.","operationId":"logout_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/whoami":{"get":{"tags":["oauth"],"summary":"Oauth Whoami","description":"Tell the consent UI which handle is currently logged in.\n\nUsed by /site/oauth-consent.html to render \"Sarah, claude.ai\nwants to...\" instead of \"Someone's claude.ai...\".","operationId":"oauth_whoami_oauth_whoami_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Oauth Whoami Oauth Whoami Get"}}}}}}},"/account/info":{"get":{"tags":["oauth"],"summary":"Account Info","description":"Full profile for the signed-in handle — drives account.html.\n\nKept separate from /oauth/whoami (which stays a one-field, no-DB\nidentity check used on every page's nav) so the heavier profile\nquery only runs on the account page itself. handle_id comes from\nthe session cookie, so a user can only ever read their own row.","operationId":"account_info_account_info_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Account Info Account Info Get"}}}}}}},"/v1/handles/check":{"post":{"tags":["public"],"summary":"Check Handle","description":"Pre-flight: is `handle` valid and available, given this magic-link token?\n\nThe /setup page calls this as the user types. It does NOT consume the\ntoken; that only happens on /reserve. The reservation must be live for\nthe call to go through — discourages using /check as a public handle-\nexistence oracle.","operationId":"check_handle_v1_handles_check_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Check Handle V1 Handles Check Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/handles/reserve":{"post":{"tags":["public"],"summary":"Reserve Handle","description":"One-shot: turns a pending signup into a live Tandem user.\n\nWraps three steps in a single transaction so an error anywhere rolls\neverything back:\n\n    1. UPDATE the reservation row (used_at, handle_issued). A\n       concurrent request that beat us loses the race — 0 rows\n       affected = 410 Gone.\n    2. INSERT into handles. Unique violation = 409 Conflict (handle\n       was just claimed in a different /reserve call).\n    3. INSERT into tokens bound to the new handle.\n\nReturns { handle, token, email } with the raw bearer token. Token is\nnever shown again.","operationId":"reserve_handle_v1_handles_reserve_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Reserve Handle V1 Handles Reserve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/handles/issue-reservation":{"post":{"tags":["admin"],"summary":"Issue Reservation","description":"Admin-only: generate a reservation token for a signup email.\n\nReturns the raw token (shown once). The admin is responsible for\ntriggering the welcome email with the raw token (see\ntandem.api.mail.send_welcome_email). Token-generation and email-send\nare deliberately separate so we can re-send the link without creating\na new reservation, and so email delivery failures don't leave the\ntable in an inconsistent state.","operationId":"issue_reservation_v1_admin_handles_issue_reservation_post","parameters":[{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Issue Reservation V1 Admin Handles Issue Reservation Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/passkeys/register/start":{"post":{"tags":["passkeys"],"summary":"Register Start","description":"Begin passkey registration for the authenticated handle.\n\nReturns publicKeyCredentialCreationOptions for\nnavigator.credentials.create(). Client prompts the user to add a\npasskey (Face ID etc), then posts the result back to /register/finish.","operationId":"register_start_v1_passkeys_register_start_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Register Start V1 Passkeys Register Start Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/passkeys/register/finish":{"post":{"tags":["passkeys"],"summary":"Register Finish","description":"Verify the attestation and persist the new passkey.","operationId":"register_finish_v1_passkeys_register_finish_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Register Finish V1 Passkeys Register Finish Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/passkeys/authenticate/start":{"post":{"tags":["passkeys"],"summary":"Authenticate Start","description":"Begin passkey login.\n\nIf the client includes a `handle` hint, we return the credential IDs\nregistered for that handle so the browser can prompt for the right\none (username-first flow). If not (usernameless / discoverable\ncredentials flow), we return only the challenge — the authenticator\nresolves which credential to use on-device.","operationId":"authenticate_start_v1_passkeys_authenticate_start_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Authenticate Start V1 Passkeys Authenticate Start Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/passkeys/authenticate/finish":{"post":{"tags":["passkeys"],"summary":"Authenticate Finish","description":"Verify the assertion and sign the user in (web session cookie).","operationId":"authenticate_finish_v1_passkeys_authenticate_finish_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Authenticate Finish V1 Passkeys Authenticate Finish Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/login/magic/request":{"post":{"tags":["public"],"summary":"Magic Request","description":"Issue + email a one-shot sign-in link for `handle`.\n\nAlways returns the same shape regardless of whether the handle\nexists or has an email on file — the response must not be a\nhandle-existence or email-existence oracle.","operationId":"magic_request_login_magic_request_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Magic Request Login Magic Request Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/login/magic":{"get":{"tags":["public"],"summary":"Magic Consume","description":"Consume a magic token and sign the browser in.","operationId":"magic_consume_login_magic_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}},{"name":"next","in":"query","required":false,"schema":{"type":"string","default":"/inbox.html","title":"Next"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/login/code/request":{"post":{"tags":["public"],"summary":"Code Request","description":"Issue + email a one-shot numeric sign-in code for `handle`.\n\nThe code sibling of magic_request — same email lookup, same no-oracle\ngeneric response, same hashed one-shot storage in passkey_challenges.\nThe difference is the credential (a short numeric code, not a link\ntoken) and the verify step (a POST, below), so the whole sign-in stays\ninside the iOS home-screen PWA.","operationId":"code_request_login_code_request_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Code Request Login Code Request Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/login/code/verify":{"post":{"tags":["public"],"summary":"Code Verify","description":"Verify a typed code and sign the browser in — IN PLACE.\n\nPOST (not a GET link) so Set-Cookie comes back on a response the page's\nown fetch() receives: in the iOS home-screen PWA that lands the session\ncookie in the PWA's cookie box, which the magic LINK can't do (it opens\nSafari). Founder = user parity: this mints the exact same tandem_session\nvia _create_session/_set_session_cookie as every other login path.","operationId":"code_verify_login_code_verify_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/login/google":{"get":{"tags":["public"],"summary":"Google Start","description":"Kick off the Google OAuth dance.","operationId":"google_start_login_google_get","parameters":[{"name":"next","in":"query","required":false,"schema":{"type":"string","default":"/inbox.html","title":"Next"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/login/google/callback":{"get":{"tags":["public"],"summary":"Google Callback","description":"Handle Google's redirect back: code -> tokens -> email -> session.","operationId":"google_callback_login_google_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"type":"string","default":"","title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"type":"string","default":"","title":"State"}},{"name":"error","in":"query","required":false,"schema":{"type":"string","default":"","title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/inbox-data.json":{"get":{"tags":["preview"],"summary":"Inbox Data","operationId":"inbox_data_inbox_data_json_get","parameters":[{"name":"key","in":"query","required":false,"schema":{"type":"string","default":"","title":"Key"}},{"name":"handle","in":"query","required":false,"schema":{"type":"string","default":"","title":"Handle"}}],"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","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Healthz Healthz Get"}}}}}}},"/v1/version":{"get":{"tags":["ops"],"summary":"Version Endpoint","description":"Who am I and when did I boot?\n\nFields:\n  - commit_sha:       full SHA if available, else \"unknown\"\n  - commit_sha_short: first 7 chars for UI display\n  - version:          Python package version (tandem.__version__)\n  - boot_time:        ISO-8601 UTC — when THIS app instance started\n  - now:              ISO-8601 UTC — current server time","operationId":"version_endpoint_v1_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Version Endpoint V1 Version Get"}}}}}}},"/v1/workflow-conventions":{"get":{"tags":["conventions"],"summary":"Workflow Conventions","description":"Return the active workflow-conventions ruleset, ordered for\nclient consumption.","operationId":"workflow_conventions_v1_workflow_conventions_get","parameters":[{"name":"audience","in":"query","required":false,"schema":{"type":"string","description":"Optional audience filter — 'all' (default), 'mcp', 'cli', or 'web'. Returns rules whose audience matches OR is 'all'.","default":"all","title":"Audience"},"description":"Optional audience filter — 'all' (default), 'mcp', 'cli', or 'web'. Returns rules whose audience matches OR is 'all'."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Workflow Conventions V1 Workflow Conventions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/inbound/postmark":{"post":{"summary":"Inbound Postmark","description":"Postmark inbound webhook + Cloudflare Email Worker target.\n\nShared-secret validation: Postmark is configured to POST to\n``/inbound/postmark?token=<TANDEM_POSTMARK_INBOUND_TOKEN>``. The\nquery-string token matches the ``TANDEM_POSTMARK_INBOUND_TOKEN`` env\nvar. Any mismatch returns 401 without parsing. Empty env var =\naccept unauthenticated (dev-mode only — do NOT run in prod with the\ntoken unset).\n\nFlow:\n  1. Validate shared-secret query param\n  2. Parse Postmark inbound JSON → envelope dict (tandem.api.inbound)\n  3. Persist via repository.save_envelope()\n  4. Return the created message_id/thread_id so Postmark's retry\n     logic can see a 2xx and move on.","operationId":"inbound_postmark_inbound_postmark_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Inbound Postmark Inbound Postmark Post"}}}}}}},"/v1/messages":{"post":{"summary":"Post Message","description":"Accept a Tandem envelope from a CLI (TandemApiTransport.send) or an\nMCP client.\n\nAuth scopes:\n  - admin (X-Tandem-Api-Key): unrestricted — can post with any from_handle.\n  - bearer (Authorization: Bearer <token>): restricted — from_handle\n    must match the token's bound handle. Prevents a user's Claude\n    connector from impersonating anyone else on Tandem.\n  - session (web cookie): restricted the same way — a signed-in human\n    sending from the web inbox can only send AS THEMSELVES. This is the\n    path that lets any logged-in user (founders included, no special\n    casing) reply from the webpage, human-typed, with no token.","operationId":"post_message_v1_messages_post","parameters":[{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}},{"name":"Authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Envelope"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Message V1 Messages Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Get Messages","description":"Return messages addressed to `handle` (or sent by it) with\nsent_at > `since`. Used by TandemApiTransport.sync() and eventually\nby MCP read_inbox / list_threads tools.\n\nAuth scopes:\n  - admin (X-Tandem-Api-Key): can query for any handle.\n  - bearer (Authorization: Bearer <token>): can ONLY query for the\n    handle the token is bound to. Prevents reading other users' mail.","operationId":"get_messages_v1_messages_get","parameters":[{"name":"for","in":"query","required":true,"schema":{"type":"string","description":"Handle to fetch messages for","title":"For"},"description":"Handle to fetch messages for"},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO 8601 timestamp; returns messages sent_at > this","title":"Since"},"description":"ISO 8601 timestamp; returns messages sent_at > this"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}},{"name":"Authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Messages V1 Messages Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/threads/{thread_id}/visibility":{"post":{"summary":"Post Thread Visibility","description":"Set a thread's visibility (active | archived | junk) for the\ncalling handle. This is a PER-HANDLE overlay — it never touches the\nshared corpus; the thread stays recoverable and immutable.\n\nAuth scopes:\n  - admin (X-Tandem-Api-Key): may set visibility for any handle via\n    a \"handle\" field in the body (operator tooling).\n  - bearer / session: sets it ONLY for the handle it is bound to.\n    Visibility is a personal view, so a handle is required.","operationId":"post_thread_visibility_v1_threads__thread_id__visibility_post","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"string","title":"Thread Id"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}},{"name":"Authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Thread Visibility V1 Threads  Thread Id  Visibility Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/attachments/{attachment_id}/download":{"get":{"summary":"Download Attachment","description":"Stream an attachment's bytes — the web inbox's 📎 download link.\n\nSame participant scoping as the fetch_attachment MCP verb: a bearer or\nsession caller must be a participant on the attachment's thread (or its\noriginal uploader); admin is unrestricted. Serves with Content-\nDisposition: attachment so the browser downloads rather than renders.","operationId":"download_attachment_v1_attachments__attachment_id__download_get","parameters":[{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","title":"Attachment Id"}},{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}},{"name":"Authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/attachments":{"post":{"summary":"Upload Attachment","description":"Upload a file from the web composer (the paperclip button).\n\nAccepts JSON ``{filename, content_base64, content_type?}`` (base64 keeps\nthe upload on the same JSON path as the rest of the API — no multipart\ndependency). Stores the blob and creates an attachment row owned by the\nsigned-in handle; ``message_id`` stays NULL until the message is sent,\nwhich binds it via ``bind_owned_attachments_to_message``. Same 25MB cap\nas the ``attach`` verb. Returns the new attachment_id + metadata.","operationId":"upload_attachment_v1_attachments_post","parameters":[{"name":"X-Tandem-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Tandem-Api-Key"}},{"name":"Authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Upload Attachment V1 Attachments Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_token_oauth_token_post":{"properties":{"grant_type":{"type":"string","title":"Grant Type"},"client_id":{"type":"string","title":"Client Id"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"redirect_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Redirect Uri"},"code_verifier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Verifier"},"resource":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resource"},"refresh_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refresh Token"},"scope":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope"}},"type":"object","required":["grant_type","client_id"],"title":"Body_token_oauth_token_post"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"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"}}}}