{"openapi":"3.1.0","info":{"title":"Engram Control Plane","description":"\nThe Engram control plane: document bases, ingestion, onboarding jobs, retrieval-backed chat, and the\nMCP surface an agent talks to.\n\n## Versions\n\n**`/v1` is the contract.** Paths under `/v1` are stable: a field is added, never removed or\nretyped, and a route keeps its meaning. Write clients against it. `GET /` lists the versions this\ndeployment serves; `GET /v1` is the version document.\n\n**The unversioned paths are for the bundled clients.** They are what the web console and older CLI\ninstalls already call, so they keep working unchanged. They are frozen, not supported: new\nconventions land on `/v1` only, and when an unversioned path is retired it will first\nanswer with a `Deprecation` header carrying the removal date, for at least one full release cycle\nbefore anything stops.\n\n## Conventions under `/v1`\n\n**Errors.** Every failure is `{\"error\": {\"code\", \"message\", \"status\", \"details\"?, \"request_id\"}}`.\nSwitch on `code`, show `message`, quote `request_id` to support. `Retry-After` is a header on the\nrefusals that have one.\n\n**Pagination.** List routes take `limit` (default 50, max 500) and an opaque `cursor`, and answer\n`{\"items\": [...], \"next_cursor\": ...}` with `X-Total-Count` and `X-Next-Cursor` headers. Follow\n`next_cursor` until it is null.\n\n**Idempotency.** Send `Idempotency-Key` on any POST/PUT/DELETE. A retry with the same key returns the\nfirst response and `Idempotent-Replayed: true` instead of doing the work twice; records live 24\nhours. Reusing a key with a different body is a 422 `idempotency_mismatch`.\n\n**Request ids.** Every response carries `X-Request-Id`, echoed if you send one.\n\n## Authentication\n\n`Authorization: Bearer <token>` takes either a tenant API key (`ek_...`, scoped to ingest / query /\nadmin) or a session token. The hosted MCP server at `/mcp-http/` takes the same API key.\n","version":"1"},"paths":{"/auth/register":{"post":{"tags":["auth"],"summary":"Register","description":"Create a workspace and its first admin.\n\nTWO MODES, one route (config.SIGNUP_MODE):\n\n  invite (default) - today's invite-only beta, unchanged. Gated by ALLOW_REGISTRATION; the\n    workspace lands on the legacy \"beta\" plan sentinel and the creator is verified on the spot\n    (they proved control of the address by setting a password here).\n\n  open (CAAS-1406) - self-serve signup. ALLOW_REGISTRATION no longer applies (open means open);\n    the abuse controls run first (the per-IP rate limit above, the disposable-domain blocklist,\n    and the one-account-per-address uniqueness the 409 below already enforces); the workspace\n    starts on config.signup_default_plan() and the admin starts UNVERIFIED with a link in their\n    inbox — and with NO session (PLAT-18): this route returns an empty access_token and\n    email_verified=false, /auth/login refuses the password, and deps refuses any token, until\n    that link is clicked.\n\n    The default is FREE (the 2026-09-11 repricing): no card, no expiry, and a hard stop at the\n    Free caps, so somebody who signs up has a working document base in one step instead of a\n    clock and a payment form. SIGNUP_DEFAULT_PLAN=trial restores the 14-day card-required run\n    for an environment that wants it - the only thing that changes is which row of the plan\n    table gets stamped here.\n\nThe second rate limit is OPEN-MODE ONLY and keyed on the REMOTE ADDRESS specifically, not on\nthe shared tenant-or-IP key: a signup has no tenant yet, and in open mode this is the one\nunauthenticated route that creates durable state, so it gets its own tighter per-IP ceiling\n(config.SIGNUP_RATE_LIMIT, read through a lambda so an operator can change it without a code\nchange). In invite mode it is exempt — the beta's 10/minute above is the control there, and a\nhuman already approved every account that gets created.","operationId":"register_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/verify-email":{"get":{"tags":["auth"],"summary":"Verify Email Route","description":"Confirm an address from the emailed link. Single-use: the stored hash is cleared on success,\nso a replayed link is simply invalid. Expiry is measured from email_verify_sent_at\n(config.EMAIL_VERIFY_EXPIRE_HOURS) - one column covers both the expiry and the resend cooldown.\nAn already-verified account whose token was cleared gets the same 400 as a bad token, which is\nright: the link really is spent.","operationId":"verify_email_route_auth_verify_email_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/resend-verification":{"post":{"tags":["auth"],"summary":"Resend Verification","description":"Send the verification link again. ALWAYS returns {status:\"sent\"} whether or not the address\nhas an account, and whether or not it is already verified - the same no-enumeration rule as\n/auth/forgot-password. The per-email cooldown (signup_guard) is the mail-bomb control, and this\nis where it belongs: registration itself is one-per-address (User.email is unique, a repeat is\na 409), so resending is the only repeatable per-address action in the flow.","operationId":"resend_verification_auth_resend_verification_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendVerificationReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/request-access":{"post":{"tags":["auth"],"summary":"Request Access","description":"Invite-only-beta waitlist: record a pending access request a platform_admin\napproves later. Idempotent-ish: a repeat request from the same email while one is\nstill pending doesn't stack duplicates. Always returns {status:'pending'} — it\nnever reveals whether the email already has an account.","operationId":"request_access_auth_request_access_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestAccessReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/accept-invite":{"post":{"tags":["auth"],"summary":"Accept Invite","description":"Redeem a teammate/approval invite: create (or activate) the user with the given\npassword + role, then return a session token so they're signed in immediately.","operationId":"accept_invite_auth_accept_invite_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInviteReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/invite-info":{"post":{"tags":["auth"],"summary":"Invite Info","description":"Who is this invite for? The accept page shows \"joining {workspace} as {email}\" so\nthe invitee can confirm before setting a password. Reveals nothing the token holder\ncouldn't learn by accepting (the token IS the proof of invitation); token rides the\nPOST body so it stays out of URLs/logs.","operationId":"invite_info_auth_invite_info_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteInfoReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteInfoResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/forgot-password":{"post":{"tags":["auth"],"summary":"Forgot Password","description":"Start a password reset, or set a first password. ALWAYS returns {status:'sent'} regardless of\nwhether the email exists (no user enumeration). When EMAIL_BACKEND=none the reset_link is\nincluded in the body so the flow is testable.\n\nPLAT-33 widened this to accounts with NO local password — the ones Google or a customer's SSO\nprovider created. It used to skip them silently, which left a Google customer who wanted a\npassword with nowhere to go and a sign-in screen pointing at a flow that did nothing. Handing\nthem a grant is safe for the same reason it is safe for everyone else: the link goes to the\naddress on the account, and that address is one the identity provider already vouched for. It is\nalso not a way around anything — a workspace that enforces SSO still refuses a password at\n/auth/login, whatever the row holds.","operationId":"forgot_password_auth_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/reset-password":{"post":{"tags":["auth"],"summary":"Reset Password","description":"Consume a reset grant and set the new password. Single-use: the grant is marked\nused so the link can't be replayed.","operationId":"reset_password_auth_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/login":{"post":{"tags":["auth"],"summary":"Login","description":"`remember_me` (an extra form field beside the OAuth2 pair) mints the long-lived\n\"remember me on this device\" session; the client pairs it with persistent storage.","operationId":"login_auth_login_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_auth_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/refresh":{"post":{"tags":["auth"],"summary":"Refresh Session","description":"Slide the session forward (PLAT-8): swap a token that is past half its life for a fresh one\nof the SAME class, until the session hits its absolute cap.\n\nWhy this is safe to expose to any signed-in caller: every check that guards the rest of the API\nhas already run by the time the body executes. `get_current_user` verified the signature and the\nexpiry, matched `tv` against the user's current token_version (so a password reset kills a\nsession here too), refused a deactivated account, a suspended workspace and an unverified\naddress (PLAT-18 — so a session cannot be slid forward past the confirmation it never had), and\napplied the workspace IP allowlist. What is left is purely lifetime arithmetic, and it reads the\nclass and the sign-in time off the presented token — never off the request — so no caller can talk its way\nfrom a short session into a persistent one, or past its own absolute cap.\n\n`tv` is re-read from the database here rather than copied from the old token, so a renewal\nalways carries the CURRENT version: copying it forward would let a stale session renew itself\npast the reset that was supposed to end it.","operationId":"refresh_session_auth_refresh_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/me":{"get":{"tags":["auth"],"summary":"Me","operationId":"me_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/setup":{"post":{"tags":["auth"],"summary":"Workspace Setup","description":"PLAT-32 — name the workspace the Google sign-up just created, and the person in it.\n\nSigning up with an email address asks for a workspace name on the form. Signing up with Google\nnever could, because the whole flow is two redirects, so the callback guesses \"<name>'s\nworkspace\" and this is where the guess gets corrected. One route, both fields, because it is one\nscreen and one save.\n\nIt is deliberately NOT a signup-only route. It renames the caller's OWN workspace and their OWN\ndisplay name, so there is nothing a second rename could do that this does not already cover, and\na settings page that wants one later should call this rather than grow a rival. Admin only, for\nthe obvious reason: a workspace's name belongs to whoever runs it, and every member can see it.\n\nThe validation is RegisterReq's and AcceptInviteReq's, reused wholesale in WorkspaceSetupReq —\nsame trim, same caps, same refusal for a name that was only spaces — because a name typed here\nmust be a name the signup form would have accepted.","operationId":"workspace_setup_auth_setup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceSetupReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/config":{"get":{"tags":["auth"],"summary":"Auth Config","description":"Lets the SPA show the 'Continue with Google' button only when configured, and pick the right\nsignup flow: signup_mode \"open\" renders a sign-up form, \"invite\" renders the waitlist form.\nbilling_enabled tells it whether a card-required sign-up will be sent to Checkout first, and\ndefault_plan says which tier a new workspace lands on so the copy can match it.","operationId":"auth_config_auth_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/google/login":{"get":{"tags":["auth"],"summary":"Google Login","description":"Step 1: bounce the browser to Google's consent screen.","operationId":"google_login_auth_google_login_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/google/callback":{"get":{"tags":["auth"],"summary":"Google Callback","description":"Step 2: Google redirects back here. Verify, find-or-create the tenant+user,\nthen hand the SPA our own JWT via the URL fragment (kept out of server logs).","operationId":"google_callback_auth_google_callback_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/link-info":{"post":{"tags":["auth"],"summary":"Link Info","description":"Who is this linking screen about? The page says \"this Google account will be linked to the\nEngram account for {email}\", so it needs the address the ticket names.\n\nSame shape and the same reasoning as /auth/invite-info: the ticket is the proof, it rides the\nPOST body rather than a URL, and it tells the holder nothing they could not learn by pressing\nContinue. An expired or forged ticket gets one message, whichever it was.","operationId":"link_info_auth_link_info_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkAccountReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkAccountInfoResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/link":{"post":{"tags":["auth"],"summary":"Link Account","description":"Record the link the person just agreed to, and sign them in.\n\nThis is the only route that turns a Google sign-in into a session for an account that already\nhad a password, and the ticket is the only thing that authorises it — which is why it is checked\nhere again rather than trusted because the callback issued it five minutes ago.\n\nEvery rule the callback applies still applies. A workspace that enforces SSO refuses, because\nconsent to link is not consent to bypass the directory its admin made mandatory. A deactivated\naccount refuses. And the stamp is what ends the screen: from here both methods sign into the\nsame workspace and neither asks again.","operationId":"link_account_auth_link_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkAccountReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/sso/start":{"post":{"tags":["auth"],"summary":"Sso Start","description":"Step 1: turn an email address into the customer's own sign-in page.\n\nPUBLIC, and deliberately says nothing about whether the ADDRESS has an account. It answers a\nquestion about the DOMAIN — \"does this company sign in through their own provider?\" — which is\na fact their own login page already advertises. A domain with no SSO gets a 404 the sign-in\nscreen reads as \"use a password instead\".","operationId":"sso_start_auth_sso_start_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoStartReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoStartResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/sso/callback":{"get":{"tags":["auth"],"summary":"Sso Callback","description":"Step 2: the customer's provider redirects here.\n\nEvery failure lands back on the sign-in page with an `?error=` the SPA already knows how to\nrender, because this is a browser redirect target — a JSON error body would leave someone\nlooking at a blank page. The success path is identical to the Google one: our own session JWT in\nthe URL fragment, which keeps it out of server logs.\n\nThe security rules worth stating plainly:\n  - the TENANT comes from the signed state, never from the token, so a callback can only ever\n    produce a user inside the workspace that started the flow;\n  - the id_token is verified against that tenant's issuer's JWKS with the nonce from the same\n    signed state (app/sso.verify_id_token);\n  - an email that already belongs to a DIFFERENT workspace is refused outright. Without that,\n    any customer's identity provider could assert any address and sign in as somebody else's\n    user, which is the one way a per-tenant SSO feature can go badly wrong;\n  - SSO never creates a workspace. A person whose address is not yet a member joins the tenant\n    that owns the domain, as a member, and the seat cap still applies.","operationId":"sso_callback_auth_sso_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"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/members":{"get":{"tags":["admin"],"summary":"List Members","description":"Members of the caller's tenant + invites that haven't been accepted yet.","operationId":"list_members_admin_members_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembersResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/invites":{"post":{"tags":["admin"],"summary":"Create Invite","description":"Invite a teammate into the caller's workspace. Returns the accept-invite link\nwhen EMAIL_BACKEND=none; otherwise emails it and omits it from the response.","operationId":"create_invite_admin_invites_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteCreateReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/invites/{invite_id}":{"delete":{"tags":["admin"],"summary":"Revoke Invite","description":"Revoke a pending invite. Tenant-scoped: an admin can only revoke their own\ntenant's invites (a cross-tenant id 404s, not 403, to avoid leaking existence).","operationId":"revoke_invite_admin_invites__invite_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"invite_id","in":"path","required":true,"schema":{"type":"string","title":"Invite Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/members/{user_id}":{"patch":{"tags":["admin"],"summary":"Update Member Role","description":"Change a member's workspace role (admin<->member), scoped to the tenant. An\nadmin can't demote themselves if they're the last admin (avoids locking the\nworkspace out of its own /admin/*).","operationId":"update_member_role_admin_members__user_id__patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleUpdateReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["admin"],"summary":"Remove Member","description":"Remove a member from the workspace (tenant-scoped). An admin can't remove\nthemselves via this endpoint, and can't remove the last admin.","operationId":"remove_member_admin_members__user_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/usage":{"get":{"tags":["admin"],"summary":"Tenant Usage","description":"Usage rollup for the caller's workspace: totals + per-corpus breakdown + a ~30-day daily\nquery series. Corpora/documents/storage are strictly tenant-scoped; the query series is the\ndeployment-level served-query signal (Measurement carries no tenant_id). Zeros/empties when\nthere's no data yet — never errors.","operationId":"tenant_usage_admin_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/billing":{"get":{"tags":["admin"],"summary":"Tenant Billing","description":"Billing shell for the caller's workspace (Stripe deferred): the plan + its limits, current\nusage against them, and an estimated $/period from the pricing rate card. `plan` is the\ntenant's stored plan (default 'beta'); the cost comes from pricing.estimate_cost_usd so the\nnumber matches the platform-admin cost-per-tenant view.","operationId":"tenant_billing_admin_billing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api-keys":{"get":{"tags":["api-keys"],"summary":"List Api Keys","description":"This workspace's keys, newest first. Revoked keys are INCLUDED (with their revoked_at set):\nthe tombstone is the record that a credential was turned off, which is the thing an auditor\nwants to see. Secrets are structurally absent — ApiKeyResp has no field for one.","operationId":"list_api_keys_api_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ApiKeyResp"},"type":"array","title":"Response List Api Keys Api Keys Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["api-keys"],"summary":"Create Api Key","description":"Mint a key for the caller's workspace. The secret is in `key` and is shown HERE ONLY — only\nits SHA-256 is stored, so there is no endpoint that can ever return it again.","operationId":"create_api_key_api_keys_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreatedResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api-keys/{key_id}":{"delete":{"tags":["api-keys"],"summary":"Revoke Api Key","description":"Revoke a key: set the tombstone, keep the row. Effective on the very next request — auth\nre-reads revoked_at every time, so there is no cached-credential window.\n\nIdempotent: re-revoking an already-revoked key returns it unchanged (and writes no second audit\nrow) rather than erroring, so a retrying client can't be told a shutdown failed that succeeded.","operationId":"revoke_api_key_api_keys__key_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/status":{"get":{"tags":["billing"],"summary":"Billing Status","description":"Billing status for the caller's workspace — ALWAYS 200, safe when billing is disabled.\nSurfaces the flag, the rate card (so pricing is visible in one place), whether the\nmanage-billing portal is available, and the full plan state: which tier, which term, the Stripe\nsubscription status we mirror, when a trial runs out, when the paid period renews, whether a\ncancellation is already pending at that renewal, whether Checkout has to happen first, and the\nresolved caps the entitlement checks enforce.","operationId":"billing_status_billing_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingStatusResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/billing/checkout":{"post":{"tags":["billing"],"summary":"Billing Checkout","description":"Start a Stripe Checkout Session for a paid tier and hand back the hosted URL.\n\nSubscription mode, card ALWAYS required. `payment_method_collection=\"always\"` is the line that\nmakes the card-required trial real: without it Stripe lets a customer start a trial with no\npayment method, and day 15 is then a dead subscription instead of a conversion.\n`trial_period_days` is only passed while the tenant is actually on trial, so an upgrade from a\npaid plan never grants a second free run.\n\nENTERPRISE / INVOICE BILLING. A tier that carries the invoice_billing feature is collected by\nINVOICE on net terms, so a finance team pays the way it already pays everyone, self-serve, with\nnobody at Engram in the loop. TERMS are the benefit the plan sells, and terms alone (PLAT-20):\npaying from a US bank account is a payment method the account switches on for everybody, so the\nplan copy no longer sells it as a higher tier. Invoice collection needs the dashboard settings\nthe setup script lists at the end of a run; until those are on, Stripe rejects the session\nrather than silently downgrading it, which is the failure we want.\n\nSTRIPE TAX rides behind STRIPE_TAX_ENABLED for the same reason: it has to be configured in the\ndashboard first, so the flag and the setting flip together.\n\nONE SUBSCRIPTION PER WORKSPACE (BILL-4). Checkout MINTS a subscription, so a workspace that\nalready has a live one gets a 409 pointing at change-plan instead — the first live UAT checkout\nran twice and left the customer paying for two. The id is what proves a subscription exists:\nopen signup stamps free/active on a workspace that has never been to Stripe at all, so status\nalone would refuse every genuine first purchase. Until the webhook stamps that id there is\nnothing to be double-billed against, which is exactly when a retry should be allowed through.\n\nCURRENT LEGAL ACCEPTANCE IS REQUIRED TO BUY (require_all_current_legal): a workspace that has not\naccepted every published document gets a 403 legal_acceptance_required naming the first one and\nthe page to accept it on, because a subscription is sold under those terms. It is a dependency,\nso it answers before the body — including before the billing-disabled 503.","operationId":"billing_checkout_billing_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/billing/change-plan":{"post":{"tags":["billing"],"summary":"Billing Change Plan","description":"Move an existing subscription between tiers. Two different Stripe mechanisms, because the\nproduct rule is two different things:\n\n  UPGRADE — immediate, with proration. Subscription.modify swaps the price on the existing item\n    and `proration_behavior=\"create_prorations\"` bills the difference for the rest of the\n    period. The customer asked for more capacity; they get it on this request, and limits.py\n    reads the new cap the moment the webhook lands (or the stamp below, whichever is first).\n\n  DOWNGRADE — honored at the end of the period they already paid for. That is a subscription\n    SCHEDULE, not a modify: create a schedule from the live subscription and append a phase on\n    the new price. Stripe switches at the renewal boundary and releases the schedule afterwards,\n    so nobody loses capacity they bought and nobody is credited for time they used.\n\nA tenant with no subscription is sent to Checkout instead — there is nothing to change yet.\n\nCURRENT LEGAL ACCEPTANCE IS REQUIRED TO CHANGE A PAID PLAN (require_all_current_legal), the same\n403 the checkout route raises: moving between paid tiers is buying. The billing PORTAL stays\nungated, so an existing customer can always manage or cancel what they already bought.","operationId":"billing_change_plan_billing_change_plan_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/billing/portal":{"post":{"tags":["billing"],"summary":"Billing Portal","description":"Open the Stripe billing portal for the caller's workspace — cards, invoices, plan changes and\ncancellation, all handled by Stripe so none of it needs us. 503 while billing is disabled (the\nbeta). When enabled: lazily create the Stripe customer for the tenant if it has none yet\n(stamped with tenant_id metadata + persisted), then create a billing-portal session.","operationId":"billing_portal_billing_portal_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingPortalResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/billing/webhook":{"post":{"tags":["billing"],"summary":"Billing Webhook","description":"Stripe -> control-plane webhook. NO auth dependency: Stripe calls it, and authenticity is proven\nby the signature (verified with stripe.Webhook.construct_event against STRIPE_WEBHOOK_SECRET), not a\nuser JWT. 503 while billing is disabled; 400 on a bad/absent signature.\n\nEvery event is acknowledged 200 once the signature verifies, including ones we do nothing with:\na non-200 makes Stripe retry, and retrying an event we deliberately ignore is noise. A handler\nthat throws is logged and still acked for the same reason — Stripe cannot fix our bug by\nsending it again.","operationId":"billing_webhook_billing_webhook_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"}}}}}}},"/plans":{"get":{"tags":["plans"],"summary":"List Plans","description":"The published plan matrix. No auth: this is a pricing page.","operationId":"list_plans_plans_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlansResp"}}}}}}},"/status":{"get":{"tags":["status"],"summary":"Status","description":"Live platform status for signed-in users (any workspace member or API key), cached\nSTATUS_CACHE_S seconds. It was public until 2026-09-11; the founder chose not to advertise\noutages to the world, so the sign-in footer link went and the route now needs a session.","operationId":"status_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/status/slo":{"get":{"tags":["status"],"summary":"Status Slo","description":"The service objectives on their own — no probes, no live state, just the numbers and the\nwords. Separate from /status so the pricing and terms pages can quote them without pulling a\nhealth check, and so a change to the objectives is one fetch to verify.","operationId":"status_slo_status_slo_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Status Slo Status Slo Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/corpora":{"get":{"tags":["corpora"],"summary":"List Corpora","operationId":"list_corpora_corpora_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CorpusResp"},"type":"array","title":"Response List Corpora Corpora Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["corpora"],"summary":"Create Corpus","operationId":"create_corpus_corpora_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorpusCreateReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorpusResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/corpora/{corpus_id}":{"get":{"tags":["corpora"],"summary":"Get Corpus","operationId":"get_corpus_corpora__corpus_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorpusResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["corpora"],"summary":"Delete Corpus","description":"Delete a document base and everything under it. The whole sequence — rows, objects, retrieval\nindex, cartridge blobs, warm KV — lives in corpus_delete.delete_corpus_fully, which the UAT reset\nscript runs too, so a corpus deleted by an operator is cleaned up exactly like one a customer\ndeletes. This route is the ownership check plus the 204.","operationId":"delete_corpus_corpora__corpus_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/documents":{"post":{"tags":["corpora"],"summary":"Upload Documents","operationId":"upload_documents_corpora__corpus_id__documents_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_documents_corpora__corpus_id__documents_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocumentResp"},"title":"Response Upload Documents Corpora  Corpus Id  Documents Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["corpora"],"summary":"List Documents","description":"The corpus's documents. FILES by default (CAAS-402): a long document is stored as a parent row\nplus section rows, and the sections are an implementation detail of how it is served — the user\nuploaded one file and expects to see one row, with `section_count` telling them it was split.\n\n`?sections=true` includes the section rows for a client that wants the per-cart detail (their\nown lifecycle, their own cart id). With SECTION_CARTS off nothing has a parent, so both answers\nare the same list.","operationId":"list_documents_corpora__corpus_id__documents_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"sections","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Sections"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocumentResp"},"title":"Response List Documents Corpora  Corpus Id  Documents Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/documents/manifest.csv":{"get":{"tags":["corpora"],"summary":"Export Upload Manifest","description":"Download a CSV of the documents nobody pulled — uploaded on the website, pushed with the CLI,\nor imported one-off through the wizard's picker. The \"Website uploads\" card's manifest.\n\n`?source=upload` is the only value it takes, and it is required rather than implied so the URL\nsays what is in the file. A registered source's manifest is served by the sources router, which\nhas the source id to scope it with; this route is the other half of that pair and deliberately\ncannot be widened into \"every document in the base\" by editing the query string.\n\nA read, so any valid credential for the tenant may make it — the same rule the documents list\nfollows. Declared above DELETE /{corpus_id}/documents/{document_id} so \"manifest.csv\" is never\nread as a document id.","operationId":"export_upload_manifest_corpora__corpus_id__documents_manifest_csv_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source","in":"query","required":false,"schema":{"type":"string","pattern":"^upload$","default":"upload","title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/documents/{document_id}":{"delete":{"tags":["corpora"],"summary":"Delete Document","description":"Delete ONE document, everywhere (CAAS-1304).\n\nCorpus delete was the only way to remove anything, which made \"take this one file out of the\nmemory\" a re-upload of everything else. This is the same end-to-end path at document granularity:\nthe raw object and its text sidecar, the deleted cart's entries in the corpus chunk sidecar, the\nDB row, the retrieval index, and — when no other document of the workspace still references the\ncart — the durable cartridge and every warm copy of it.\n\nOwnership is checked TWICE and both checks 404 rather than 403: the corpus must be this tenant's\n(get_owned_corpus) and the document must belong to that corpus. Another tenant's document id is\nindistinguishable from one that never existed, so the route cannot be used to probe for ids.\n\nA build in flight does NOT block the delete. The document is removed and the receipt says a build\nwas running; the build path's compensation re-checks the CORPUS with a fresh session, finds it\nstill alive, and carries on, so a cart that run writes after our offboard lands with no document\nreferencing it — an orphan the GC sweep reclaims. Blocking instead would mean a stuck build could\nhold a deletion request open indefinitely, which is the worse failure for a right-to-erasure path.","operationId":"delete_document_corpora__corpus_id__documents__document_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/import":{"post":{"tags":["corpora"],"summary":"Start Import","description":"Import a connected source's folder into this document base. Requires ownership of the base AND\nthat the connection belong to the same tenant (a cross-tenant connection id 404s). 409 if an import\nfor this base is already running. The walk + downloads happen in the background; poll import-status.","operationId":"start_import_corpora__corpus_id__import_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportStatusResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/import-status":{"get":{"tags":["corpora"],"summary":"Import Status","description":"Latest import run for this document base, or {\"state\": \"none\"} if nothing has been imported.","operationId":"import_status_corpora__corpus_id__import_status_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportStatusResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/train":{"post":{"tags":["jobs"],"summary":"Start Training","operationId":"start_training_corpora__corpus_id__train_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"force","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Force"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/describe":{"post":{"tags":["jobs"],"summary":"Describe Corpus","description":"Regenerate the corpus-level catalog description on demand — the onboarding describe pass only\nruns post-onboard, so corpora onboarded BEFORE that pass existed (or whose description the old\ntoken budget truncated) need this to BACKFILL without a retrain. One GPU generation per call.\n\nOwner-authed exactly like start_training (tenant mismatch -> 404, so a corpus's existence never\nleaks cross-tenant) and requires a ready corpus (a mid-onboard corpus has no stable inventory to\ndescribe). Synchronously runs _write_corpus_description and returns the (possibly unchanged) value:\nthe description stays None when the pass failed or produced no complete sentence (the same\ntruncation guard the onboarding pass uses).","operationId":"describe_corpus_corpora__corpus_id__describe_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorpusDescribeResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/cancel":{"post":{"tags":["jobs"],"summary":"Cancel Training","description":"Request cancellation of the in-flight training run. We don't kill the GPU\nworker directly; we set a flag the worker reads from its progress-heartbeat\nresponse and then aborts cooperatively (maps to Temporal cancel on AWS).\n\nThe user cancels the RUN, so the flag goes on the corpus-level aggregate — the batches under it\nread it there (_cancel_requested) and the progress heartbeat echoes it, so a cancel reaches a\nbatch already parked inside the box's build call as well as the ones still queued (CAAS-202).","operationId":"cancel_training_corpora__corpus_id__cancel_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/jobs/{job_id}":{"get":{"tags":["jobs"],"summary":"Get Job","operationId":"get_job_jobs__job_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/jobs":{"get":{"tags":["jobs"],"summary":"List Jobs","operationId":"list_jobs_corpora__corpus_id__jobs_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/JobResp"},"title":"Response List Jobs Corpora  Corpus Id  Jobs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/chat":{"post":{"tags":["chat"],"summary":"Chat","operationId":"chat_corpora__corpus_id__chat_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatResp"}}}},"503":{"description":"Serving is unavailable; retry after the named delay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServingUnavailableDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/chat/stream":{"post":{"tags":["chat"],"summary":"Chat Stream","description":"Token-streaming chat (SSE): head event with sources, delta events as the model writes, then a\ndone event with measured metrics. Tenant-scoped + JWT-authed exactly like /chat. Needs a ready\ncorpus on the vLLM backend (the streaming serve path); the HF path has no token stream.","operationId":"chat_stream_corpora__corpus_id__chat_stream_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"503":{"description":"Serving is unavailable; retry after the named delay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServingUnavailableDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mcp/{corpus_id}/query":{"post":{"tags":["chat"],"summary":"Mcp Query","description":"The answer route behind the hosted MCP server's query_corpus tool. Auth is a tenant API key\nrather than a user JWT, so an agent can ask questions without a user session. It differs from\n/corpora/{id}/chat in its source attribution: the ids it returns are the PINNABLE cart ids the\ninventory route below speaks, which is what lets a follow-up pin the same evidence.","operationId":"mcp_query_mcp__corpus_id__query_post","parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatResp"}}}},"503":{"description":"Serving is unavailable; retry after the named delay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServingUnavailableDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mcp/{corpus_id}/documents":{"get":{"tags":["chat"],"summary":"Mcp Documents","description":"The inventory behind the hosted MCP server's list_documents tool. `q` is an optional\ncase-insensitive substring matched against filename OR description; `total` is the count AFTER that\nfilter so the client can page it. DB-only (no storage round-trip); doc ids are the SAME pinnable\ncurrency as /query's used_docs/doc_ids.","operationId":"mcp_documents_mcp__corpus_id__documents_get","parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpDocsResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/compare/stream":{"post":{"tags":["compare"],"summary":"Compare Stream","description":"Token-streaming version of one compare side (SSE): a `head` event with sources, `delta`\nevents as the model writes, then `summary` with measured metrics + $/query. The UI streams the\nSmart CAG side first, then RAG — modern-chatbot feel with the same honest measurement.","operationId":"compare_stream_corpora__corpus_id__compare_stream_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"side","in":"query","required":false,"schema":{"type":"string","pattern":"^(cart|rag)$","default":"cart","title":"Side"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/compare":{"post":{"tags":["compare"],"summary":"Compare","operationId":"compare_corpora__corpus_id__compare_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"queries_per_month","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":1,"default":100000,"title":"Queries Per Month"}},{"name":"side","in":"query","required":false,"schema":{"type":"string","pattern":"^(both|cart|rag)$","default":"both","title":"Side"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/scale-test/stream":{"post":{"tags":["compare"],"summary":"Scale Test Stream","description":"Stream a real concurrency ramp (SSE). Per level: {level, cart:{qps,ttft,lat,...}, rag:{...}}.","operationId":"scale_test_stream_corpora__corpus_id__scale_test_stream_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScaleTestReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/scale-runs":{"post":{"tags":["compare"],"summary":"Save Scale Run","description":"Persist a finished scale-test run (the tab POSTs its accumulated per-level points here on done).","operationId":"save_scale_run_corpora__corpus_id__scale_runs_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScaleRunSaveReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScaleRunResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["compare"],"summary":"List Scale Runs","description":"Past scale-test runs for this corpus, newest first (points included — runs are small).","operationId":"list_scale_runs_corpora__corpus_id__scale_runs_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScaleRunResp"},"title":"Response List Scale Runs Corpora  Corpus Id  Scale Runs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/economics":{"get":{"tags":["economics"],"summary":"Economics","operationId":"economics_corpora__corpus_id__economics_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"queries_per_month","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":1,"default":100000,"title":"Queries Per Month"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/metrics/cost-comparison":{"get":{"tags":["metrics"],"summary":"Cost Comparison","description":"Cartridge (this platform) vs RAG on the same open model. `measured` = the real per-query latency\n/ prefill / $ collected at run time on THIS deployment (empty until the first live query); `modeled`\n= the scenario projection for the corpus-size / volume sliders. The UI shows measured when present\nand labels the modeled projection as such.","operationId":"cost_comparison_metrics_cost_comparison_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_tokens","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":1000,"default":1000000,"title":"Corpus Tokens"}},{"name":"queries_per_month","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":1,"default":100000,"title":"Queries Per Month"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/metrics/savings":{"get":{"tags":["metrics"],"summary":"Savings","description":"Deployment-level LIFETIME aggregates from the persisted measurements (authenticated — see\nmodule docstring): per-side totals (count, avg latency, avg $/query), the estimated\ncumulative savings of the cart path vs RAG (per-query cost delta x the number served), and a\nper-month breakdown. Empty/zeroed until the first live query has been recorded.","operationId":"savings_metrics_savings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/audit":{"get":{"tags":["audit"],"summary":"List Audit Events","description":"The caller's tenant's audit events, newest first. `limit` defaults to 100 (max 1000). Filtered\nstrictly on the JWT's tenant_id — a tenant never sees another tenant's (or the _system GC) receipts.","operationId":"list_audit_events_audit_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AuditEventResp"},"title":"Response List Audit Events Audit Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audit/deletions":{"get":{"tags":["audit"],"summary":"List Deletion Receipts","description":"The tenant's DELETION receipts, newest first (CAAS-1304).\n\nWHY a route of its own rather than a filter on /audit: a deletion receipt is the one audit row a\ncustomer is entitled to go and find after the fact, and the obvious place to look for it —\nGET /corpora/{id}/deletion-receipts — is impossible by construction, because the corpus whose id\nyou would use is exactly what the delete removed. So the receipts are addressable at the TENANT\nlevel instead. Each row's `detail` carries the per-tier outcome (see routers/corpora.py), whose\ntier keys line up with GET /platform/erasure-timeline.\n\nTenant-ADMIN rather than any member: the receipts name every document base the workspace has ever\ndeleted, which is a workspace-wide history, not the caller's own activity.","operationId":"list_deletion_receipts_audit_deletions_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AuditEventResp"},"title":"Response List Deletion Receipts Audit Deletions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audit/export":{"get":{"tags":["audit"],"summary":"Export Audit","description":"Download this workspace's audit log.\n\nThe point of the feature: a compliance reviewer asks \"show me everything that happened to this\ndata\", and the answer is a file, not a screenshot of a table. Oldest first, because a log reads\nforward. `jsonl` keeps `detail` as the JSON it is, one event per line, which is what a pipeline\nwants; `csv` is what a spreadsheet wants.\n\nTenant-scoped by construction — the tenant comes from the credential, never from a parameter, so\nthere is no id here for anyone to change.","operationId":"export_audit_audit_export_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","pattern":"^(csv|jsonl|ndjson|json)$","default":"jsonl","title":"Format"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date or timestamp, inclusive","title":"Since"},"description":"ISO date or timestamp, inclusive"},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date or timestamp, exclusive","title":"Until"},"description":"ISO date or timestamp, exclusive"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/platform/erasure-timeline":{"get":{"tags":["platform"],"summary":"Get Erasure Timeline","description":"What happens to every copy of your data when you delete it, and how long each copy can live.\nGenerated from the constants this module enforces against, so it can never drift from the\nlifecycle rules and TTLs actually configured.","operationId":"get_erasure_timeline_platform_erasure_timeline_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ErasureStageResp"},"type":"array","title":"Response Get Erasure Timeline Platform Erasure Timeline Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/models":{"get":{"tags":["models"],"summary":"List Model Tiers","description":"The model tiers shown in the onboarding 'choose model' step. `available` tiers are\nselectable now; the rest render as 'coming soon' until a serving engine is wired. Does not\nexpose the internal `model_ref` — the client sends a tier `id` at onboarding and the backend\nmaps it to weights (serving.model_ref_for_tier).","operationId":"list_model_tiers_models_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Model Tiers Models Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/connectors":{"get":{"tags":["connectors"],"summary":"List Source Connectors","description":"The ingestion sources shown in the 'connect a source' menu. `available` connectors are\nselectable now; the rest render as 'coming soon' until their OAuth app credentials + the token\nencryption key are configured. No connector's secrets are exposed — only the availability flag\nderived from them.","operationId":"list_source_connectors_connectors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Source Connectors Connectors Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/connectors/{provider}/authorize":{"get":{"tags":["connectors"],"summary":"Authorize","description":"Step 1 of connect: return the provider consent URL for the SPA to redirect ITSELF to (we don't\n302 an XHR). The user must own the document base being connected. corpus_id + the user id are\ncarried in Authlib's signed-session state so the callback can't be tampered to target another base\nor user.","operationId":"authorize_connectors__provider__authorize_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"corpus_id","in":"query","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorAuthorizeResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/connectors/{provider}/callback":{"get":{"tags":["connectors"],"summary":"Callback","description":"Step 2 of connect: the provider redirects here with a code. Exchange it, fetch the account\nlabel, UPSERT the connection for (tenant, provider, account) with the tokens ENCRYPTED at rest,\nthen redirect back to the base's setup page. Any failure redirects with ?connector_error=<provider>\n(never a raw traceback to the browser).","operationId":"callback_connectors__provider__callback_get","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/connectors/google_drive_shared/connect-picked":{"post":{"tags":["connectors"],"summary":"Shared Drive Connect Picked","description":"Make a Picker folder pick actually import: share the folder with the service account on the\nuser's behalf, then connect it exactly as /connect would.\n\nThe user's OAuth connection carries a drive.file grant on the folder they just picked, and that\ngrant covers the folder's permissions, so the app may add the service account as a viewer. A\nreal Drive share cascades to the folder's contents — which the Picker grant never did, and which\nis why folder picks walked to 0 added. The customer never leaves the app and no scope changes.\n\nShare refusals are not fatal on their own: if the service account can already read the folder\n(a re-pick of a folder shared earlier, or a subfolder of one), the connect proceeds. Only a\nrefusal that leaves the folder unreadable is reported, and it is reported with Google's own\nreason, because the advice that follows from a sharing rate limit is the opposite of the advice\nthat follows from a permissions problem.\n\nOwnership is judged against the Google account that did the picking, not only against the\naddress the person signs in to Engram with: those are routinely different, and judging by the\nlogin alone refused people their own folders.","operationId":"shared_drive_connect_picked_connectors_google_drive_shared_connect_picked_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SharedFolderConnectPickedReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/connectors/google_drive_shared/identity":{"get":{"tags":["connectors"],"summary":"Shared Drive Identity","description":"The address a customer shares a folder with. 404 like every unconfigured source, so the UI can\nleave the option out rather than show one that cannot work.","operationId":"shared_drive_identity_connectors_google_drive_shared_identity_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SharedFolderIdentityResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/connectors/google_drive_shared/connect":{"post":{"tags":["connectors"],"summary":"Shared Drive Connect","description":"Connect a folder the customer has shared with the platform's service account.\n\nNo OAuth and no Picker: the share in Drive is the grant. The folder is read once here to prove\nthe share landed and to record its name, checked against the caller (see\n_folder_belongs_to_caller), and pinned on the connection — the import and the sync read the\nfolder from the row and never from a request. Reconnecting the same folder refreshes the row\nrather than adding a duplicate, matching the OAuth upsert rule.","operationId":"shared_drive_connect_connectors_google_drive_shared_connect_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SharedFolderConnectReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/connectors/connections":{"get":{"tags":["connectors"],"summary":"List Connections","description":"This tenant's source connections (no tokens exposed — only the account label).","operationId":"list_connections_connectors_connections_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ConnectionResp"},"type":"array","title":"Response List Connections Connectors Connections Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/connectors/connections/{connection_id}":{"delete":{"tags":["connectors"],"summary":"Disconnect Connection","description":"Disconnect a source: revoke the grant upstream (best-effort) and DELETE the stored connection\nrow. No body (204). After this the Connect button starts a FRESH OAuth — the frontend skips OAuth\nonly while a connection row exists, so dropping the row is what re-enables reconnecting.\n\nThis exists because a stored-but-dead connection was unrecoverable: a user revoked the app from\nGoogle's side (myaccount.google.com/connections), and our stale row made the SPA skip OAuth and\ndead-end on a failed picker-config call with no way out (found live). Deleting the row is the fix;\nrevoke_tokens also tells Google to forget its half so the user doesn't have to.","operationId":"disconnect_connection_connectors_connections__connection_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","title":"Connection Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/connectors/connections/{connection_id}/browse":{"get":{"tags":["connectors"],"summary":"Browse","description":"One level of the connected source's tree: subfolders to drill into + a count of importable files\nhere. Ids are opaque provider refs the client passes straight back (see providers module).\n\nThis drives the SharePoint in-app picker ONLY. Google Drive folder selection happens client-side in\nthe Google Picker (see /picker-config) — under drive.file a server-side browse would only ever see\nthe already-granted subset, and the Picker grants + selects in one step — so the drive path here is\nexercised only for a folder id the Picker already handed back.","operationId":"browse_connectors_connections__connection_id__browse_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","title":"Connection Id"}},{"name":"folder_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Folder Id"}},{"name":"site_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Site Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrowseResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/connectors/connections/{connection_id}/picker-config":{"get":{"tags":["connectors"],"summary":"Picker Config","description":"The config the browser needs to open the Google Picker for this connection. Google Drive only:\nunder the drive.file scope the user grants + selects a folder inside Google's own Picker, which must\nrun with the user's Drive access token — so we mint/refresh a valid one and hand it back along with\nthe project's app id + Picker API key. The token is short-lived, never logged, never persisted\nclient-side (the Picker session consumes it).","operationId":"picker_config_connectors_connections__connection_id__picker_config_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","title":"Connection Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PickerConfigResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/onboarding":{"get":{"tags":["onboarding"],"summary":"Get Onboarding","description":"Read the wizard state so the frontend reopens at the right step with per-file progress.","operationId":"get_onboarding_corpora__corpus_id__onboarding_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingStateResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["onboarding"],"summary":"Patch Onboarding","description":"Persist the wizard cursor and/or the chosen model tier as the user moves through the steps.\n`model_tier` is validated against the serving registry (unknown ids rejected) — a placeholder\n(disabled) tier is still a VALID selection here so the review step can show it as 'coming soon';\nthe availability gate is enforced at /onboard, not at selection time.","operationId":"patch_onboarding_corpora__corpus_id__onboarding_patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingPatchReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingStateResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/estimate":{"get":{"tags":["onboarding"],"summary":"Estimate","description":"Review step (4): a pre-run sizing summary — doc count, detected file types (from filename\nextensions), total bytes, and a coarse estimated onboarding time + cost. The estimate constants\nlive in ONE place (metrics.onboard_estimate); the real figures land on the corpus after the run\n(see /corpora/{id}/economics).","operationId":"estimate_corpora__corpus_id__estimate_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/onboard":{"post":{"tags":["onboarding"],"summary":"Onboard","description":"Step 5: start onboarding server-side. Resolve the chosen tier to concrete weights and dispatch\nvia the EXISTING job path (jobs.dispatch_training -> ml_client onboard/train, progress + cancel\nreused). The worker sets onboarding_step='ready' on success.\n\nGATE (current placeholder reality): if the chosen tier is not `available` (no enabled serving\nengine yet), dispatch NOTHING — return {\"status\": \"no_serving_engine\"} at HTTP 409 and leave the\ncursor at 'review' so the UI shows 'onboarding starts once a model is enabled'. This keeps the\nwhole flow buildable/testable through step 4 with no GPU.","operationId":"onboard_corpora__corpus_id__onboard_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingStateResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/feedback":{"post":{"tags":["feedback"],"summary":"Submit Feedback","operationId":"submit_feedback_corpora__corpus_id__feedback_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnswerFeedbackReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/sources":{"post":{"tags":["sources"],"summary":"Create Source","description":"Register a bucket this document base pulls from. Always 201; the `status` says whether it\nworks yet.\n\nREGISTRATION CANNOT REQUIRE A WORKING ROLE, because of the order the customer has to do things\nin. The trust policy they apply names an external id, and the external id comes from us — so the\nonly sequence that can succeed is: get an id, render the policy from it, apply it, register.\nValidating at registration and refusing on failure made the FIRST call in that sequence the one\nthat had to already work, which it cannot. Two changes fix it, and both are here:\n\n  * the caller may supply the `external_id` they already built their policy around (checked for\n    shape and for collision), and we mint one only when they have not;\n  * a failed AssumeRole is no longer fatal. The source is kept at `pending_validation` with the\n    plain reason on it, and the response carries the external id and the CloudFormation and\n    Terraform to apply — so a customer who registered first still has everything they need in\n    front of them. `POST .../validate` (or their first sync) flips it to `ready`.\n\nWhat stays fatal is anything the CUSTOMER got wrong in the request itself: a malformed bucket\nname or role ARN, a cross-region bucket they did not opt into, a duplicate prefix, an external id\nthat is too short. Those are 422s and 409s, because no amount of waiting fixes them.\n\nA `google_drive` or `sharepoint` source (CAAS-606) takes the other branch below: there is no role\nto assume and no bucket to list, so what gets checked is that the named connection is this\ntenant's, and the first sync is what proves the folder is readable.","operationId":"create_source_corpora__corpus_id__sources_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceCreateReq"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceCreateResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sources"],"summary":"List Sources","description":"The sources feeding this document base. A read, so any valid credential for the tenant may\nmake it — scopes limit what a key can change or spend, not what it can see of its own workspace.","operationId":"list_sources_corpora__corpus_id__sources_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SourceResp"},"title":"Response List Sources Corpora  Corpus Id  Sources Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/sources/{source_id}":{"put":{"tags":["sources"],"summary":"Update Source","description":"Change a registered source in place: how it pulls, how often, and which role it uses.\n\nThe bucket and the prefix are not here, because they are what a customer's trust policy was\nwritten around and changing them in place would silently repoint a standing grant. That is a\nnew registration rather than an edit. Everything else is a setting people genuinely revise:\nthe mode, the schedule, the S3 Inventory destination (CAAS-604), and the credential.\n\nTHE CREDENTIAL IS THE POINT OF THIS ROUTE. A role ARN typed wrong used to leave a source that\ncould never sync and could never be repaired, only deleted and re-registered, which meant\nre-applying the IAM template over a typo. Changing the role or the external id re-checks access\nstraight away and reports the verdict.\n\nOmitted fields are left alone, so a client sends only what it is changing, and `model_fields_set`\nis what separates \"not mentioned\" from \"cleared\" — sending `schedule_minutes` as null turns the\nschedule off, while leaving it out keeps it. Sending both inventory fields empty turns inventory\noff and the next sync goes back to listing, which always works.\n\nThe access verdict is returned but not fatal, for the same reason registration is not: the\ncustomer may be applying the grant and editing the source in either order, and the first\ninventory report takes up to 48 hours to arrive. The templates come back whenever there is\nstill something to apply. A cross-region bucket is the one exception, as it is at registration:\nthat refusal is about cost, and waiting never fixes it.","operationId":"update_source_corpora__corpus_id__sources__source_id__put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceUpdateReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceCreateResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sources"],"summary":"Delete Source","description":"Stop pulling from a bucket.\n\nA hard delete of the source and its object rows, not a tombstone, and that is the right call for\nthis table: the rows exist only to answer \"what have we already fetched from this bucket\", a\nquestion that stops being meaningful the moment we stop fetching. Documents already ingested are\nuntouched — removing a source ends future syncs and does not retract what a past one delivered.\nThe cached role session is dropped too, so credentials for a bucket we no longer serve do not sit\nin process memory for the rest of the hour.\n\nPLAT-29: the documents this source fed have their `source_id` NULLED rather than deleted, so they\nmove under the Documents tab's \"Website uploads\" card instead of vanishing with the card that was\nshowing them. That is the same promise the paragraph above makes, made good in the one place a\ngrouped view could have broken it: the documents are still there, and now they are still visible.","operationId":"delete_source_corpora__corpus_id__sources__source_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/sources/{source_id}/manifest.csv":{"get":{"tags":["sources"],"summary":"Export Source Manifest","description":"Download a CSV of the documents THIS source has fed into the base (PLAT-29).\n\nOne row per file, with the state it is in, why it is in that state, when it last moved and the\nhash of its text. It replaces scrolling a list of every document in the base: the question people\nactually had was \"what did this folder give me and is any of it broken\", which is a source-shaped\nquestion and a spreadsheet-shaped answer.\n\nIts twin is GET /corpora/{id}/documents/manifest.csv?source=upload, the same columns for the\ndocuments nobody pulled. Both build their rows with `corpora.document_manifest_rows`, so the two\nfiles always agree about what a column means.\n\nA read, so any valid credential for the tenant may make it — scopes limit what a key can change\nor spend, not what it can see of its own workspace (the same rule the sources list follows).","operationId":"export_source_manifest_corpora__corpus_id__sources__source_id__manifest_csv_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/sources/{source_id}/validate":{"post":{"tags":["sources"],"summary":"Validate Source","description":"Re-check a source: assume the role, list one page, record the verdict.\n\nThe one people actually use. A source goes to `error` when a customer rotates a policy or renames\na bucket, and this is how they confirm the fix without waiting for a sync. Unlike registration a\nfailure here does NOT delete the row: the source already existed and the customer may well be\nmid-repair, so the row stays with `error` and the reason on it.","operationId":"validate_source_corpora__corpus_id__sources__source_id__validate_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceValidationResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/sources/{source_id}/sync":{"post":{"tags":["sources"],"summary":"Sync Source","description":"Pull from this source now, and return the run to watch.\n\nThe request does the cheap half and returns: for S3 it computes the diff (a listing, no GETs) and\nqueues one download job per new or changed key; for Drive and SharePoint it queues the delta walk.\nNothing downloads inside this call, so a fifty-thousand-object bucket answers as fast as an empty\none.\n\n409 when a run for this source is already going. Two concurrent walks of one bucket would fetch\nthe same keys twice and race each other's `source_objects` writes, and the second one adds\nnothing — whatever it would find, the first is already finding.\n\n409 as well on a source that has never validated. It is registered but the trust policy is not\nworking yet (registration no longer refuses that — see create_source), and starting a run that\ncould only fail would put a failed run in the customer's history for a problem that is not a run\nproblem. The sync RETRIES the validation first, though, so a customer who applied their policy\nafter registering just clicks sync and it works — which is the \"or the first sync\" half of the\nflow, without them having to know there was a separate step.","operationId":"sync_source_corpora__corpus_id__sources__source_id__sync_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncRunResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sources/s3/setup":{"get":{"tags":["sources"],"summary":"S3 Setup","description":"The IAM setup for one source, as CloudFormation and as Terraform.\n\nTakes the external id as a parameter rather than reading it from a source row, because the useful\norder is backwards from the obvious one: a customer wants the templates BEFORE the source exists,\napplies them, and registers with the role ARN that comes out. The endpoint is a pure renderer\nover the values it is given, so it works either way round.\n\nThat also means it holds no secret to leak. The external id is the caller's own input, and the\noutput is a policy document computed from it — supplying someone else's external id renders a\ntemplate that grants access to a role only they can create in an account only they control.","operationId":"s3_setup_sources_s3_setup_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"external_id","in":"query","required":true,"schema":{"type":"string","title":"External Id"}},{"name":"bucket","in":"query","required":true,"schema":{"type":"string","title":"Bucket"}},{"name":"prefix","in":"query","required":false,"schema":{"type":"string","default":"","title":"Prefix"}},{"name":"kms_key_arn","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kms Key Arn"}},{"name":"inventory_bucket","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inventory Bucket"}},{"name":"inventory_prefix","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inventory Prefix"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceSetupResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/upload-credentials":{"post":{"tags":["sources"],"summary":"Upload Credentials","description":"An hour of S3 credentials scoped to this document base's prefix, and the command to use them.\n\nThis is what makes \"push into Engram\" work with tools we did not write. Any S3 client speaks\nthese credentials, so a customer can use `aws s3 sync`, DataSync, or bucket replication without\nEngram being in the data path at all — the API task never holds a byte of the file.\n\nAn hour is the ceiling, not a preference: the task is already running under an assumed role, and\nSTS caps a chained session at one hour. Long enough for a large sync, short enough that a leaked\nset stops working the same afternoon.","operationId":"upload_credentials_corpora__corpus_id__upload_credentials_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadCredentialsResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/platform/info":{"get":{"tags":["sources"],"summary":"Platform Info","description":"Where Engram runs, and what that means for your bill.\n\nPublic, and deliberately so: the question \"which region should my bucket be in\" is one people\nhave before they have an account, and answering it behind a login guarantees somebody creates the\nbucket in the wrong place first. Deployment facts only — nothing about tenants, volumes, or\ncapacity.\n\nServed from config rather than written down anywhere, so the region a customer reads and the\nregion the platform actually runs in cannot disagree.","operationId":"platform_info_platform_info_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformInfoResp"}}}}}}},"/enterprise/sso":{"get":{"tags":["enterprise"],"summary":"Get Sso","description":"The workspace's SSO configuration, secret-free.\n\nNOT plan-gated: an admin on any plan can look at this page and see the redirect URI they would\nneed. The gate is on writing, which is where the feature actually costs something.","operationId":"get_sso_enterprise_sso_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoConfigResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"put":{"tags":["enterprise"],"summary":"Put Sso","description":"Configure single sign-on for this workspace.\n\nValidated against the real provider before anything is stored: we fetch the issuer's discovery\ndocument and check it names the same issuer and carries the endpoints the flow needs. An admin\nwho mistypes the issuer finds out here, on the screen where they typed it, instead of at their\nfirst employee's first login.\n\nDomain uniqueness is enforced across the whole deployment. POST /auth/sso/start maps an address\nto a workspace through this list, so a domain claimed twice would have two answers and we would\nbe choosing which company's identity provider gets to vouch for a person. Refuse instead.","operationId":"put_sso_enterprise_sso_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoConfigReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoConfigResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"delete":{"tags":["enterprise"],"summary":"Delete Sso","description":"Turn single sign-on off and forget the configuration, secret included.\n\nNOT plan-gated, deliberately: a workspace that drops to a plan without SSO has to be able to\nturn it off, and enforcement is what would otherwise lock everyone out. Turning a security\nfeature OFF is never the thing to charge for.","operationId":"delete_sso_enterprise_sso_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoConfigResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/enterprise/sso/test":{"post":{"tags":["enterprise"],"summary":"Test Sso","description":"Hand the admin an authorization URL they can open in a new tab.\n\nThis is the \"does it actually work\" button. It builds the same URL a real sign-in would, against\nthe same cached client, so a wrong client id or an unregistered redirect URI shows up as the\nprovider's own error message on the provider's own page, which is the clearest possible place\nfor it to appear.","operationId":"test_sso_enterprise_sso_test_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoTestResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/enterprise/ip-allowlist":{"get":{"tags":["enterprise"],"summary":"Get Ip Allowlist","description":"The stored list, plus the address this request came from so the page can offer it as the\nfirst entry. Not plan-gated for the same reason as GET /enterprise/sso.","operationId":"get_ip_allowlist_enterprise_ip_allowlist_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IpAllowlistResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"put":{"tags":["enterprise"],"summary":"Put Ip Allowlist","description":"Restrict this workspace to a set of networks.\n\nThe lockout guard is the whole reason this route is more than a setter. An admin who saves a\nlist their own address falls outside has just locked their company out of its own account, and\nthe next request, including the one that would undo it, is refused. So the write refuses first\nand explains what it saw. `confirm_lockout=true` is the deliberate override, because the case is\nreal: an admin at home configuring the office range means it.\n\nAn empty list clears the restriction and is always allowed.","operationId":"put_ip_allowlist_enterprise_ip_allowlist_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IpAllowlistReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IpAllowlistResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/enterprise/kms-key":{"get":{"tags":["enterprise"],"summary":"Get Kms Key","operationId":"get_kms_key_enterprise_kms_key_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KmsKeyResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"put":{"tags":["enterprise"],"summary":"Put Kms Key","description":"Point this workspace's documents at a key the customer owns.\n\nGated on `kms_key`, which is ENTERPRISE ONLY. It used to borrow a broader flag, which also\nlet Business in — but Enterprise's pricing copy sells the customer key as something Business\ndoes not have (\"Everything in Business plus IP allowlists on each workspace and your own KMS\nkey on your documents\"), so the borrowed flag was refusing on the wrong line. FEAT-1 gives the\nkey its own flag and puts it on the tier the copy actually sells it on.\n\nSETTING A KEY IS GATED, CLEARING ONE IS NOT — which is why the check is inside the handler on\n`check_plan_feature` rather than on the route through `require_plan_feature`. The tier moved,\nso there are workspaces on Business that set a key back when Business carried the feature; if\nthe whole route were gated, those customers could neither keep managing the key nor take it\noff, which is a trap of our making. Clearing is the way out and it stays open on every tier.\nThe auth level is unchanged either way: tenant admin, same as every other route here.\n\nDocuments already written with a customer key keep being decrypted with it whatever plan the\nworkspace is on, because S3 reads the key off the object; nothing on this route touches stored\ndata. A downgrade that made a customer's own documents unreadable would be a far worse outcome\nthan an unenforced promise.\n\nClearing the key returns the workspace to the bucket's default encryption: existing objects\nkeep the key they were written with, and new writes use the default, which is the honest\nbehaviour of S3 and worth saying out loud.","operationId":"put_kms_key_enterprise_kms_key_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KmsKeyReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KmsKeyResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/usage/export":{"get":{"tags":["enterprise"],"summary":"Export Usage","description":"Download this workspace's usage for a month.\n\nSame plan flag as the audit export: a finance or compliance team that needs one almost always\nneeds the other, and splitting them would add a plan row nobody asked for.","operationId":"export_usage_usage_export_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","pattern":"^(csv|jsonl|ndjson|json)$","default":"csv","title":"Format"}},{"name":"month","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"YYYY-MM; defaults to the current month","title":"Month"},"description":"YYYY-MM; defaults to the current month"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/legal/status":{"get":{"tags":["legal"],"summary":"Legal Status","description":"Which current versions this workspace has accepted.\n\nAny member, not just an admin: everyone in a workspace can reasonably want to know whether the\nterms are signed, and this returns no personal data beyond the name of whoever clicked.\n\n`all_current` is the one flag a client needs — it is what the checkout gate and the \"please\naccept the updated terms\" banner both key off, so neither has to re-derive it.","operationId":"legal_status_legal_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalStatusResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/legal/{document}":{"get":{"tags":["legal"],"summary":"Get Legal Document","description":"The current text of a document. PUBLIC: someone has to be able to read the terms before they\nhave an account, and a legal document that needs a login to read is not much of a legal\ndocument.","operationId":"get_legal_document_legal__document__get","parameters":[{"name":"document","in":"path","required":true,"schema":{"type":"string","title":"Document"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalDocResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/legal/{document}/receipt":{"get":{"tags":["legal"],"summary":"Get Legal Receipt","description":"The acceptance receipt for this workspace: the accepted text, plus who accepted it, when, and\nfrom where.\n\nThis is what a downloadable signed copy would have been. A PDF is out of scope for this tranche,\nand a JSON receipt carrying the exact text is the more useful artifact anyway: it is the same\nfacts, it diffs, and a client can render or print it however it likes.\n\nTenant ADMIN rather than any member, matching /audit/deletions: the receipt names an individual\nand the address they acted from, which is workspace evidence rather than general reading.","operationId":"get_legal_receipt_legal__document__receipt_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"document","in":"path","required":true,"schema":{"type":"string","title":"Document"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalReceiptResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/legal/accept":{"post":{"tags":["legal"],"summary":"Accept Legal","description":"Accept a document on behalf of the workspace.\n\nTENANT ADMIN, because accepting binds the whole workspace and a member cannot bind their\nemployer. The version in the request must match the published one: a page that has been open\nsince before a version bump must not be able to accept the new text by accident, so a stale\nversion is refused with what the current one is.\n\nIdempotent. A double-clicked button, or a second admin clicking the same thing, returns the\nacceptance that already exists rather than writing a duplicate row a later audit would have to\nreconcile — the unique index on (tenant, document, version) enforces the same thing in the\ndatabase.","operationId":"accept_legal_legal_accept_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalAcceptReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalDocStatusResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/corpora/{corpus_id}/documents/diff":{"post":{"tags":["ingest"],"summary":"Diff Manifest","description":"What of this manifest do we not already have?\n\nThe comparison is on `raw_sha256` — the hash of the RAW BYTES — because that is the only hash a\nclient can compute. It has the file; it does not have our parser. The server's other hash (of the\nextracted text) answers a different question, \"does this need rebuilding\", and is not on this\nwire at all.\n\nA row whose raw hash is NULL counts as CHANGED, never as unchanged. Those are documents that\nlanded before this column existed, and \"we cannot prove it is the same\" has to mean \"send it\" —\nthe opposite default would silently skip a file forever.","operationId":"diff_manifest_corpora__corpus_id__documents_diff_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManifestReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiffResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/documents/upload-urls":{"post":{"tags":["ingest"],"summary":"Upload Urls","description":"Somewhere to PUT each file, without the bytes coming through here.\n\nOn S3 that is a presigned single-part PUT straight to the corpus key layout `storage.py` already\nuses, so the object lands exactly where the parse job will look for it and no copy step exists.\nOn the local backend — a laptop, the test suite — presigning is impossible, so the URL points at\nthis API's own `documents/raw` route, which streams the body to storage. Same client flow either\nway, which is the whole point: the CLI implements one thing.\n\nSize is checked HERE rather than at commit because this is the last moment before the bytes move;\nrefusing afterwards would mean the customer paid to upload something we then rejected.","operationId":"upload_urls_corpora__corpus_id__documents_upload_urls_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlsReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlsResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/documents/raw":{"put":{"tags":["ingest"],"summary":"Put Raw Document","description":"The local backend's presigned-PUT stand-in: take a raw body and write it to storage.\n\nIt registers NOTHING. That is deliberate and it is what makes the two backends behave the same:\non S3 the bytes land in a bucket and no row exists until commit, so here too. Commit is the only\nplace a Document row is created, whichever backend is underneath.\n\nGuarded by the same ingest scope and the same 500 MB ceiling as the URL that points at it.\n\nTHE CEILING IS ENFORCED BEFORE THE BYTES ARE HELD, in two steps, because reading the whole body\nand then measuring it means the limit only ever describes what we already paid for: one\nauthenticated caller could make this task buffer 500 MB of a body it was always going to refuse,\nand a handful of them could do it concurrently. So a Content-Length over the limit is refused\nwithout reading anything, and the body is then accumulated chunk by chunk and refused the moment\nthe running total passes the ceiling — which is what covers a chunked or unlabelled body, where\nthere is no header to believe in the first place.","operationId":"put_raw_document_corpora__corpus_id__documents_raw_put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"path","in":"query","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/documents/commit":{"post":{"tags":["ingest"],"summary":"Commit Manifest","description":"Register what was uploaded and start the work.\n\nThe order of operations is the contract, and each step is where it is for a reason:\n\n  1. Quotas FIRST, on the whole manifest, before a single row exists. A refusal here has cost the\n     customer nothing but the objects they already put in the bucket, and a partial commit is\n     never a state anyone has to reason about.\n  2. Rows next, at lifecycle `pending` with the raw hash and size from the manifest. The row is\n     idempotent on (corpus, path), so re-committing updates in place.\n  3. Deletions, when `delete_missing` is set — through the same per-document delete path the API\n     uses, so carts, sidecars and the index all go with them.\n  4. One parse job per document, and only then the response. Nothing in this request reads a\n     byte of any document.\n\nA file the diff calls UNCHANGED is counted and skipped: no job, no GPU time, no lifecycle change.\nThat is what makes a sync of a base that did not move free.","operationId":"commit_manifest_corpora__corpus_id__documents_commit_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommitReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommitResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/documents/upsert":{"post":{"tags":["ingest"],"summary":"Upsert Document","description":"Write ONE document by path, content included (CAAS-607).\n\nThe integrator's endpoint. An agent writing a note, or a pipeline emitting markdown, already has\nthe content in hand, and making it presign, PUT and commit for one small file would be three\nround trips to save a copy that does not need saving. Idempotent on `path`: the same path twice\nupdates the same row, which is what \"upsert\" has to mean for a pipeline that reruns.\n\nIt still goes through the queue rather than parsing inline, so a push and a bulk commit produce\nthe same lifecycle, the same run history, and the same \"done\" signal.","operationId":"upsert_document_corpora__corpus_id__documents_upsert_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/sync-runs":{"get":{"tags":["ingest"],"summary":"List Sync Runs","description":"This base's ingestion history, newest first — every path in one list, including the wizard's\nconnector imports, which mirror into the same table.","operationId":"list_sync_runs_corpora__corpus_id__sync_runs_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SyncRunResp"},"title":"Response List Sync Runs Corpora  Corpus Id  Sync Runs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/corpora/{corpus_id}/sync-runs/{run_id}":{"get":{"tags":["ingest"],"summary":"Get Sync Run","description":"One run. The corpus is checked as well as the run, so a valid id from the wrong base 404s\nrather than leaking that it exists — the same rule the document routes follow.","operationId":"get_sync_run_corpora__corpus_id__sync_runs__run_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncRunResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhooks":{"get":{"tags":["ingest","webhooks"],"summary":"List Webhooks","description":"This workspace's endpoints. A read, so any member of the tenant may make it.\n\nTHE SECRET IS ADMIN-ONLY (`secret: null` for everyone else). Subscribing and unsubscribing are\nboth tenant-admin gated, and the secret is the HMAC key that lets anything holding it forge a\ndelivery our receiver will accept as ours — so handing it to every member and to every API key,\nwhatever scopes that key was narrowed to, gave the listing more authority than the routes that\ncreate and destroy the thing it lists. The listing itself stays open: knowing which endpoints a\nworkspace calls is ordinary workspace state, and a member who can see a misconfigured URL can\nreport it. An admin who needs the key back still reads it here; nothing else changed.","operationId":"list_webhooks_webhooks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/WebhookResp"},"type":"array","title":"Response List Webhooks Webhooks Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["ingest","webhooks"],"summary":"Create Webhook","description":"Subscribe an endpoint. Tenant admin only: a webhook streams a workspace's activity to a URL\nof someone's choosing, which is an admin decision and not a per-member one.","operationId":"create_webhook_webhooks_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreateReq"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/webhooks/{webhook_id}/rotate":{"post":{"tags":["ingest","webhooks"],"summary":"Rotate Webhook Secret","description":"Issue a new signing secret and invalidate the old one immediately.\n\nTHE OLD SECRET STOPS WORKING ON THIS CALL — there is no overlap window, because the reason to\nrotate is that the old key is not trusted any more and a grace period would keep the thing you\nare rotating away from alive for the length of it. The cost is a receiver that verifies with the\nold key rejecting deliveries until it is redeployed, which is the customer's own sequencing to\nplan and is written down on the docs page next to this route.\n\nTenant admin only, same as subscribing: whoever can read the key back can forge a delivery.\nDeliberately NOT plan-gated — a workspace that dropped a tier still has to be able to replace a\nleaked secret, and making that an upgrade prompt would be an ugly thing to do to someone in the\nmiddle of an incident.","operationId":"rotate_webhook_secret_webhooks__webhook_id__rotate_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhooks/{webhook_id}":{"patch":{"tags":["ingest","webhooks"],"summary":"Update Webhook","description":"Pause or resume an endpoint.\n\nA paused endpoint gets NOTHING: `subscribers()` filters on `active`, so no new event is ever\nqueued for it, and `deliver()` re-reads the row and returns \"skipped\" instead of raising, so a\nretry already sitting on the queue when the pause landed stops there too. Nothing is buffered\nwhile it is off, so resuming starts from the next event and never replays the gap — see\nWebhookUpdateReq for why that is the right default.\n\nKept apart from rotate rather than folded into one PATCH: pausing is routine and reversible,\nrotating breaks every receiver that has not been redeployed, and one route that can do both\ninvites doing the second by accident.","operationId":"update_webhook_webhooks__webhook_id__patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookUpdateReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResp"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["ingest","webhooks"],"summary":"Delete Webhook","description":"Unsubscribe. A real delete, not a tombstone: unlike an API key, nothing downstream points at\na webhook row, and a customer removing an endpoint means \"stop calling it\", not \"keep the\nrecord\". The audit event is the record.","operationId":"delete_webhook_webhooks__webhook_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/register":{"post":{"tags":["auth"],"summary":"Register","description":"Create a workspace and its first admin.\n\nTWO MODES, one route (config.SIGNUP_MODE):\n\n  invite (default) - today's invite-only beta, unchanged. Gated by ALLOW_REGISTRATION; the\n    workspace lands on the legacy \"beta\" plan sentinel and the creator is verified on the spot\n    (they proved control of the address by setting a password here).\n\n  open (CAAS-1406) - self-serve signup. ALLOW_REGISTRATION no longer applies (open means open);\n    the abuse controls run first (the per-IP rate limit above, the disposable-domain blocklist,\n    and the one-account-per-address uniqueness the 409 below already enforces); the workspace\n    starts on config.signup_default_plan() and the admin starts UNVERIFIED with a link in their\n    inbox — and with NO session (PLAT-18): this route returns an empty access_token and\n    email_verified=false, /auth/login refuses the password, and deps refuses any token, until\n    that link is clicked.\n\n    The default is FREE (the 2026-09-11 repricing): no card, no expiry, and a hard stop at the\n    Free caps, so somebody who signs up has a working document base in one step instead of a\n    clock and a payment form. SIGNUP_DEFAULT_PLAN=trial restores the 14-day card-required run\n    for an environment that wants it - the only thing that changes is which row of the plan\n    table gets stamped here.\n\nThe second rate limit is OPEN-MODE ONLY and keyed on the REMOTE ADDRESS specifically, not on\nthe shared tenant-or-IP key: a signup has no tenant yet, and in open mode this is the one\nunauthenticated route that creates durable state, so it gets its own tighter per-IP ceiling\n(config.SIGNUP_RATE_LIMIT, read through a lambda so an operator can change it without a code\nchange). In invite mode it is exempt — the beta's 10/minute above is the control there, and a\nhuman already approved every account that gets created.","operationId":"v1_register_v1_auth_register_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/verify-email":{"get":{"tags":["auth"],"summary":"Verify Email Route","description":"Confirm an address from the emailed link. Single-use: the stored hash is cleared on success,\nso a replayed link is simply invalid. Expiry is measured from email_verify_sent_at\n(config.EMAIL_VERIFY_EXPIRE_HOURS) - one column covers both the expiry and the resend cooldown.\nAn already-verified account whose token was cleared gets the same 400 as a bad token, which is\nright: the link really is spent.","operationId":"v1_verify_email_route_v1_auth_verify_email_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/resend-verification":{"post":{"tags":["auth"],"summary":"Resend Verification","description":"Send the verification link again. ALWAYS returns {status:\"sent\"} whether or not the address\nhas an account, and whether or not it is already verified - the same no-enumeration rule as\n/auth/forgot-password. The per-email cooldown (signup_guard) is the mail-bomb control, and this\nis where it belongs: registration itself is one-per-address (User.email is unique, a repeat is\na 409), so resending is the only repeatable per-address action in the flow.","operationId":"v1_resend_verification_v1_auth_resend_verification_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendVerificationReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/request-access":{"post":{"tags":["auth"],"summary":"Request Access","description":"Invite-only-beta waitlist: record a pending access request a platform_admin\napproves later. Idempotent-ish: a repeat request from the same email while one is\nstill pending doesn't stack duplicates. Always returns {status:'pending'} — it\nnever reveals whether the email already has an account.","operationId":"v1_request_access_v1_auth_request_access_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestAccessReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/accept-invite":{"post":{"tags":["auth"],"summary":"Accept Invite","description":"Redeem a teammate/approval invite: create (or activate) the user with the given\npassword + role, then return a session token so they're signed in immediately.","operationId":"v1_accept_invite_v1_auth_accept_invite_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInviteReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/invite-info":{"post":{"tags":["auth"],"summary":"Invite Info","description":"Who is this invite for? The accept page shows \"joining {workspace} as {email}\" so\nthe invitee can confirm before setting a password. Reveals nothing the token holder\ncouldn't learn by accepting (the token IS the proof of invitation); token rides the\nPOST body so it stays out of URLs/logs.","operationId":"v1_invite_info_v1_auth_invite_info_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteInfoReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteInfoResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/forgot-password":{"post":{"tags":["auth"],"summary":"Forgot Password","description":"Start a password reset, or set a first password. ALWAYS returns {status:'sent'} regardless of\nwhether the email exists (no user enumeration). When EMAIL_BACKEND=none the reset_link is\nincluded in the body so the flow is testable.\n\nPLAT-33 widened this to accounts with NO local password — the ones Google or a customer's SSO\nprovider created. It used to skip them silently, which left a Google customer who wanted a\npassword with nowhere to go and a sign-in screen pointing at a flow that did nothing. Handing\nthem a grant is safe for the same reason it is safe for everyone else: the link goes to the\naddress on the account, and that address is one the identity provider already vouched for. It is\nalso not a way around anything — a workspace that enforces SSO still refuses a password at\n/auth/login, whatever the row holds.","operationId":"v1_forgot_password_v1_auth_forgot_password_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/reset-password":{"post":{"tags":["auth"],"summary":"Reset Password","description":"Consume a reset grant and set the new password. Single-use: the grant is marked\nused so the link can't be replayed.","operationId":"v1_reset_password_v1_auth_reset_password_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/login":{"post":{"tags":["auth"],"summary":"Login","description":"`remember_me` (an extra form field beside the OAuth2 pair) mints the long-lived\n\"remember me on this device\" session; the client pairs it with persistent storage.","operationId":"v1_login_v1_auth_login_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_v1_login_v1_auth_login_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/refresh":{"post":{"tags":["auth"],"summary":"Refresh Session","description":"Slide the session forward (PLAT-8): swap a token that is past half its life for a fresh one\nof the SAME class, until the session hits its absolute cap.\n\nWhy this is safe to expose to any signed-in caller: every check that guards the rest of the API\nhas already run by the time the body executes. `get_current_user` verified the signature and the\nexpiry, matched `tv` against the user's current token_version (so a password reset kills a\nsession here too), refused a deactivated account, a suspended workspace and an unverified\naddress (PLAT-18 — so a session cannot be slid forward past the confirmation it never had), and\napplied the workspace IP allowlist. What is left is purely lifetime arithmetic, and it reads the\nclass and the sign-in time off the presented token — never off the request — so no caller can talk its way\nfrom a short session into a persistent one, or past its own absolute cap.\n\n`tv` is re-read from the database here rather than copied from the old token, so a renewal\nalways carries the CURRENT version: copying it forward would let a stale session renew itself\npast the reset that was supposed to end it.","operationId":"v1_refresh_session_v1_auth_refresh_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/me":{"get":{"tags":["auth"],"summary":"Me","operationId":"v1_me_v1_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/auth/setup":{"post":{"tags":["auth"],"summary":"Workspace Setup","description":"PLAT-32 — name the workspace the Google sign-up just created, and the person in it.\n\nSigning up with an email address asks for a workspace name on the form. Signing up with Google\nnever could, because the whole flow is two redirects, so the callback guesses \"<name>'s\nworkspace\" and this is where the guess gets corrected. One route, both fields, because it is one\nscreen and one save.\n\nIt is deliberately NOT a signup-only route. It renames the caller's OWN workspace and their OWN\ndisplay name, so there is nothing a second rename could do that this does not already cover, and\na settings page that wants one later should call this rather than grow a rival. Admin only, for\nthe obvious reason: a workspace's name belongs to whoever runs it, and every member can see it.\n\nThe validation is RegisterReq's and AcceptInviteReq's, reused wholesale in WorkspaceSetupReq —\nsame trim, same caps, same refusal for a name that was only spaces — because a name typed here\nmust be a name the signup form would have accepted.","operationId":"v1_workspace_setup_v1_auth_setup_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceSetupReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/config":{"get":{"tags":["auth"],"summary":"Auth Config","description":"Lets the SPA show the 'Continue with Google' button only when configured, and pick the right\nsignup flow: signup_mode \"open\" renders a sign-up form, \"invite\" renders the waitlist form.\nbilling_enabled tells it whether a card-required sign-up will be sent to Checkout first, and\ndefault_plan says which tier a new workspace lands on so the copy can match it.","operationId":"v1_auth_config_v1_auth_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/link-info":{"post":{"tags":["auth"],"summary":"Link Info","description":"Who is this linking screen about? The page says \"this Google account will be linked to the\nEngram account for {email}\", so it needs the address the ticket names.\n\nSame shape and the same reasoning as /auth/invite-info: the ticket is the proof, it rides the\nPOST body rather than a URL, and it tells the holder nothing they could not learn by pressing\nContinue. An expired or forged ticket gets one message, whichever it was.","operationId":"v1_link_info_v1_auth_link_info_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkAccountReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkAccountInfoResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/link":{"post":{"tags":["auth"],"summary":"Link Account","description":"Record the link the person just agreed to, and sign them in.\n\nThis is the only route that turns a Google sign-in into a session for an account that already\nhad a password, and the ticket is the only thing that authorises it — which is why it is checked\nhere again rather than trusted because the callback issued it five minutes ago.\n\nEvery rule the callback applies still applies. A workspace that enforces SSO refuses, because\nconsent to link is not consent to bypass the directory its admin made mandatory. A deactivated\naccount refuses. And the stamp is what ends the screen: from here both methods sign into the\nsame workspace and neither asks again.","operationId":"v1_link_account_v1_auth_link_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkAccountReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/auth/sso/start":{"post":{"tags":["auth"],"summary":"Sso Start","description":"Step 1: turn an email address into the customer's own sign-in page.\n\nPUBLIC, and deliberately says nothing about whether the ADDRESS has an account. It answers a\nquestion about the DOMAIN — \"does this company sign in through their own provider?\" — which is\na fact their own login page already advertises. A domain with no SSO gets a 404 the sign-in\nscreen reads as \"use a password instead\".","operationId":"v1_sso_start_v1_auth_sso_start_post","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoStartReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoStartResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/api-keys":{"post":{"tags":["api-keys"],"summary":"Create Api Key","description":"Mint a key for the caller's workspace. The secret is in `key` and is shown HERE ONLY — only\nits SHA-256 is stored, so there is no endpoint that can ever return it again.","operationId":"v1_create_api_key_v1_api_keys_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreatedResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"get":{"tags":["api-keys"],"summary":"List Api Keys","description":"This workspace's keys, newest first. Revoked keys are INCLUDED (with their revoked_at set):\nthe tombstone is the record that a credential was turned off, which is the thing an auditor\nwants to see. Secrets are structurally absent — ApiKeyResp has no field for one.","operationId":"v1_list_api_keys_v1_api_keys_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Rows per page. Default 50, maximum 500.","default":50,"title":"Limit"},"description":"Rows per page. Default 50, maximum 500."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page.","title":"Cursor"},"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ApiKeyResp_"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/api-keys/{key_id}":{"delete":{"tags":["api-keys"],"summary":"Revoke Api Key","description":"Revoke a key: set the tombstone, keep the row. Effective on the very next request — auth\nre-reads revoked_at every time, so there is no cached-credential window.\n\nIdempotent: re-revoking an already-revoked key returns it unchanged (and writes no second audit\nrow) rather than erroring, so a retrying client can't be told a shutdown failed that succeeded.","operationId":"v1_revoke_api_key_v1_api_keys__key_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora":{"post":{"tags":["corpora"],"summary":"Create Corpus","operationId":"v1_create_corpus_v1_corpora_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorpusCreateReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorpusResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"get":{"tags":["corpora"],"summary":"List Corpora","operationId":"v1_list_corpora_v1_corpora_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Rows per page. Default 50, maximum 500.","default":50,"title":"Limit"},"description":"Rows per page. Default 50, maximum 500."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page.","title":"Cursor"},"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_CorpusResp_"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}":{"get":{"tags":["corpora"],"summary":"Get Corpus","operationId":"v1_get_corpus_v1_corpora__corpus_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorpusResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"delete":{"tags":["corpora"],"summary":"Delete Corpus","description":"Delete a document base and everything under it. The whole sequence — rows, objects, retrieval\nindex, cartridge blobs, warm KV — lives in corpus_delete.delete_corpus_fully, which the UAT reset\nscript runs too, so a corpus deleted by an operator is cleaned up exactly like one a customer\ndeletes. This route is the ownership check plus the 204.","operationId":"v1_delete_corpus_v1_corpora__corpus_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"204":{"description":"Successful Response"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/documents":{"post":{"tags":["corpora"],"summary":"Upload Documents","operationId":"v1_upload_documents_v1_corpora__corpus_id__documents_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_v1_upload_documents_v1_corpora__corpus_id__documents_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocumentResp"},"title":"Response V1 Upload Documents V1 Corpora  Corpus Id  Documents Post"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"get":{"tags":["corpora"],"summary":"List Documents","description":"The corpus's documents. FILES by default (CAAS-402): a long document is stored as a parent row\nplus section rows, and the sections are an implementation detail of how it is served — the user\nuploaded one file and expects to see one row, with `section_count` telling them it was split.\n\n`?sections=true` includes the section rows for a client that wants the per-cart detail (their\nown lifecycle, their own cart id). With SECTION_CARTS off nothing has a parent, so both answers\nare the same list.","operationId":"v1_list_documents_v1_corpora__corpus_id__documents_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"sections","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Sections"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Rows per page. Default 50, maximum 500.","default":50,"title":"Limit"},"description":"Rows per page. Default 50, maximum 500."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page.","title":"Cursor"},"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_DocumentResp_"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/documents/manifest.csv":{"get":{"tags":["corpora"],"summary":"Export Upload Manifest","description":"Download a CSV of the documents nobody pulled — uploaded on the website, pushed with the CLI,\nor imported one-off through the wizard's picker. The \"Website uploads\" card's manifest.\n\n`?source=upload` is the only value it takes, and it is required rather than implied so the URL\nsays what is in the file. A registered source's manifest is served by the sources router, which\nhas the source id to scope it with; this route is the other half of that pair and deliberately\ncannot be widened into \"every document in the base\" by editing the query string.\n\nA read, so any valid credential for the tenant may make it — the same rule the documents list\nfollows. Declared above DELETE /{corpus_id}/documents/{document_id} so \"manifest.csv\" is never\nread as a document id.","operationId":"v1_export_upload_manifest_v1_corpora__corpus_id__documents_manifest_csv_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source","in":"query","required":false,"schema":{"type":"string","pattern":"^upload$","default":"upload","title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/documents/{document_id}":{"delete":{"tags":["corpora"],"summary":"Delete Document","description":"Delete ONE document, everywhere (CAAS-1304).\n\nCorpus delete was the only way to remove anything, which made \"take this one file out of the\nmemory\" a re-upload of everything else. This is the same end-to-end path at document granularity:\nthe raw object and its text sidecar, the deleted cart's entries in the corpus chunk sidecar, the\nDB row, the retrieval index, and — when no other document of the workspace still references the\ncart — the durable cartridge and every warm copy of it.\n\nOwnership is checked TWICE and both checks 404 rather than 403: the corpus must be this tenant's\n(get_owned_corpus) and the document must belong to that corpus. Another tenant's document id is\nindistinguishable from one that never existed, so the route cannot be used to probe for ids.\n\nA build in flight does NOT block the delete. The document is removed and the receipt says a build\nwas running; the build path's compensation re-checks the CORPUS with a fresh session, finds it\nstill alive, and carries on, so a cart that run writes after our offboard lands with no document\nreferencing it — an orphan the GC sweep reclaims. Blocking instead would mean a stuck build could\nhold a deletion request open indefinitely, which is the worse failure for a right-to-erasure path.","operationId":"v1_delete_document_v1_corpora__corpus_id__documents__document_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"204":{"description":"Successful Response"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/import":{"post":{"tags":["corpora"],"summary":"Start Import","description":"Import a connected source's folder into this document base. Requires ownership of the base AND\nthat the connection belong to the same tenant (a cross-tenant connection id 404s). 409 if an import\nfor this base is already running. The walk + downloads happen in the background; poll import-status.","operationId":"v1_start_import_v1_corpora__corpus_id__import_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportStatusResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/import-status":{"get":{"tags":["corpora"],"summary":"Import Status","description":"Latest import run for this document base, or {\"state\": \"none\"} if nothing has been imported.","operationId":"v1_import_status_v1_corpora__corpus_id__import_status_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportStatusResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/documents/diff":{"post":{"tags":["ingest"],"summary":"Diff Manifest","description":"What of this manifest do we not already have?\n\nThe comparison is on `raw_sha256` — the hash of the RAW BYTES — because that is the only hash a\nclient can compute. It has the file; it does not have our parser. The server's other hash (of the\nextracted text) answers a different question, \"does this need rebuilding\", and is not on this\nwire at all.\n\nA row whose raw hash is NULL counts as CHANGED, never as unchanged. Those are documents that\nlanded before this column existed, and \"we cannot prove it is the same\" has to mean \"send it\" —\nthe opposite default would silently skip a file forever.","operationId":"v1_diff_manifest_v1_corpora__corpus_id__documents_diff_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManifestReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiffResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/documents/upload-urls":{"post":{"tags":["ingest"],"summary":"Upload Urls","description":"Somewhere to PUT each file, without the bytes coming through here.\n\nOn S3 that is a presigned single-part PUT straight to the corpus key layout `storage.py` already\nuses, so the object lands exactly where the parse job will look for it and no copy step exists.\nOn the local backend — a laptop, the test suite — presigning is impossible, so the URL points at\nthis API's own `documents/raw` route, which streams the body to storage. Same client flow either\nway, which is the whole point: the CLI implements one thing.\n\nSize is checked HERE rather than at commit because this is the last moment before the bytes move;\nrefusing afterwards would mean the customer paid to upload something we then rejected.","operationId":"v1_upload_urls_v1_corpora__corpus_id__documents_upload_urls_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlsReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlsResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/documents/raw":{"put":{"tags":["ingest"],"summary":"Put Raw Document","description":"The local backend's presigned-PUT stand-in: take a raw body and write it to storage.\n\nIt registers NOTHING. That is deliberate and it is what makes the two backends behave the same:\non S3 the bytes land in a bucket and no row exists until commit, so here too. Commit is the only\nplace a Document row is created, whichever backend is underneath.\n\nGuarded by the same ingest scope and the same 500 MB ceiling as the URL that points at it.\n\nTHE CEILING IS ENFORCED BEFORE THE BYTES ARE HELD, in two steps, because reading the whole body\nand then measuring it means the limit only ever describes what we already paid for: one\nauthenticated caller could make this task buffer 500 MB of a body it was always going to refuse,\nand a handful of them could do it concurrently. So a Content-Length over the limit is refused\nwithout reading anything, and the body is then accumulated chunk by chunk and refused the moment\nthe running total passes the ceiling — which is what covers a chunked or unlabelled body, where\nthere is no header to believe in the first place.","operationId":"v1_put_raw_document_v1_corpora__corpus_id__documents_raw_put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"path","in":"query","required":true,"schema":{"type":"string","title":"Path"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"204":{"description":"Successful Response"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/documents/commit":{"post":{"tags":["ingest"],"summary":"Commit Manifest","description":"Register what was uploaded and start the work.\n\nThe order of operations is the contract, and each step is where it is for a reason:\n\n  1. Quotas FIRST, on the whole manifest, before a single row exists. A refusal here has cost the\n     customer nothing but the objects they already put in the bucket, and a partial commit is\n     never a state anyone has to reason about.\n  2. Rows next, at lifecycle `pending` with the raw hash and size from the manifest. The row is\n     idempotent on (corpus, path), so re-committing updates in place.\n  3. Deletions, when `delete_missing` is set — through the same per-document delete path the API\n     uses, so carts, sidecars and the index all go with them.\n  4. One parse job per document, and only then the response. Nothing in this request reads a\n     byte of any document.\n\nA file the diff calls UNCHANGED is counted and skipped: no job, no GPU time, no lifecycle change.\nThat is what makes a sync of a base that did not move free.","operationId":"v1_commit_manifest_v1_corpora__corpus_id__documents_commit_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommitReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommitResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/documents/upsert":{"post":{"tags":["ingest"],"summary":"Upsert Document","description":"Write ONE document by path, content included (CAAS-607).\n\nThe integrator's endpoint. An agent writing a note, or a pipeline emitting markdown, already has\nthe content in hand, and making it presign, PUT and commit for one small file would be three\nround trips to save a copy that does not need saving. Idempotent on `path`: the same path twice\nupdates the same row, which is what \"upsert\" has to mean for a pipeline that reruns.\n\nIt still goes through the queue rather than parsing inline, so a push and a bulk commit produce\nthe same lifecycle, the same run history, and the same \"done\" signal.","operationId":"v1_upsert_document_v1_corpora__corpus_id__documents_upsert_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/sync-runs":{"get":{"tags":["ingest"],"summary":"List Sync Runs","description":"This base's ingestion history, newest first — every path in one list, including the wizard's\nconnector imports, which mirror into the same table.","operationId":"v1_list_sync_runs_v1_corpora__corpus_id__sync_runs_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_SyncRunResp_"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/sync-runs/{run_id}":{"get":{"tags":["ingest"],"summary":"Get Sync Run","description":"One run. The corpus is checked as well as the run, so a valid id from the wrong base 404s\nrather than leaking that it exists — the same rule the document routes follow.","operationId":"v1_get_sync_run_v1_corpora__corpus_id__sync_runs__run_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncRunResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/webhooks":{"post":{"tags":["ingest","webhooks"],"summary":"Create Webhook","description":"Subscribe an endpoint. Tenant admin only: a webhook streams a workspace's activity to a URL\nof someone's choosing, which is an admin decision and not a per-member one.","operationId":"v1_create_webhook_v1_webhooks_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreateReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"get":{"tags":["ingest","webhooks"],"summary":"List Webhooks","description":"This workspace's endpoints. A read, so any member of the tenant may make it.\n\nTHE SECRET IS ADMIN-ONLY (`secret: null` for everyone else). Subscribing and unsubscribing are\nboth tenant-admin gated, and the secret is the HMAC key that lets anything holding it forge a\ndelivery our receiver will accept as ours — so handing it to every member and to every API key,\nwhatever scopes that key was narrowed to, gave the listing more authority than the routes that\ncreate and destroy the thing it lists. The listing itself stays open: knowing which endpoints a\nworkspace calls is ordinary workspace state, and a member who can see a misconfigured URL can\nreport it. An admin who needs the key back still reads it here; nothing else changed.","operationId":"v1_list_webhooks_v1_webhooks_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Rows per page. Default 50, maximum 500.","default":50,"title":"Limit"},"description":"Rows per page. Default 50, maximum 500."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page.","title":"Cursor"},"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_WebhookResp_"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/webhooks/{webhook_id}/rotate":{"post":{"tags":["ingest","webhooks"],"summary":"Rotate Webhook Secret","description":"Issue a new signing secret and invalidate the old one immediately.\n\nTHE OLD SECRET STOPS WORKING ON THIS CALL — there is no overlap window, because the reason to\nrotate is that the old key is not trusted any more and a grace period would keep the thing you\nare rotating away from alive for the length of it. The cost is a receiver that verifies with the\nold key rejecting deliveries until it is redeployed, which is the customer's own sequencing to\nplan and is written down on the docs page next to this route.\n\nTenant admin only, same as subscribing: whoever can read the key back can forge a delivery.\nDeliberately NOT plan-gated — a workspace that dropped a tier still has to be able to replace a\nleaked secret, and making that an upgrade prompt would be an ugly thing to do to someone in the\nmiddle of an incident.","operationId":"v1_rotate_webhook_secret_v1_webhooks__webhook_id__rotate_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/webhooks/{webhook_id}":{"patch":{"tags":["ingest","webhooks"],"summary":"Update Webhook","description":"Pause or resume an endpoint.\n\nA paused endpoint gets NOTHING: `subscribers()` filters on `active`, so no new event is ever\nqueued for it, and `deliver()` re-reads the row and returns \"skipped\" instead of raising, so a\nretry already sitting on the queue when the pause landed stops there too. Nothing is buffered\nwhile it is off, so resuming starts from the next event and never replays the gap — see\nWebhookUpdateReq for why that is the right default.\n\nKept apart from rotate rather than folded into one PATCH: pausing is routine and reversible,\nrotating breaks every receiver that has not been redeployed, and one route that can do both\ninvites doing the second by accident.","operationId":"v1_update_webhook_v1_webhooks__webhook_id__patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookUpdateReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"delete":{"tags":["ingest","webhooks"],"summary":"Delete Webhook","description":"Unsubscribe. A real delete, not a tombstone: unlike an API key, nothing downstream points at\na webhook row, and a customer removing an endpoint means \"stop calling it\", not \"keep the\nrecord\". The audit event is the record.","operationId":"v1_delete_webhook_v1_webhooks__webhook_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"204":{"description":"Successful Response"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/train":{"post":{"tags":["jobs"],"summary":"Start Training","operationId":"v1_start_training_v1_corpora__corpus_id__train_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"force","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Force"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/describe":{"post":{"tags":["jobs"],"summary":"Describe Corpus","description":"Regenerate the corpus-level catalog description on demand — the onboarding describe pass only\nruns post-onboard, so corpora onboarded BEFORE that pass existed (or whose description the old\ntoken budget truncated) need this to BACKFILL without a retrain. One GPU generation per call.\n\nOwner-authed exactly like start_training (tenant mismatch -> 404, so a corpus's existence never\nleaks cross-tenant) and requires a ready corpus (a mid-onboard corpus has no stable inventory to\ndescribe). Synchronously runs _write_corpus_description and returns the (possibly unchanged) value:\nthe description stays None when the pass failed or produced no complete sentence (the same\ntruncation guard the onboarding pass uses).","operationId":"v1_describe_corpus_v1_corpora__corpus_id__describe_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorpusDescribeResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/cancel":{"post":{"tags":["jobs"],"summary":"Cancel Training","description":"Request cancellation of the in-flight training run. We don't kill the GPU\nworker directly; we set a flag the worker reads from its progress-heartbeat\nresponse and then aborts cooperatively (maps to Temporal cancel on AWS).\n\nThe user cancels the RUN, so the flag goes on the corpus-level aggregate — the batches under it\nread it there (_cancel_requested) and the progress heartbeat echoes it, so a cancel reaches a\nbatch already parked inside the box's build call as well as the ones still queued (CAAS-202).","operationId":"v1_cancel_training_v1_corpora__corpus_id__cancel_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/jobs/{job_id}":{"get":{"tags":["jobs"],"summary":"Get Job","operationId":"v1_get_job_v1_jobs__job_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/jobs":{"get":{"tags":["jobs"],"summary":"List Jobs","operationId":"v1_list_jobs_v1_corpora__corpus_id__jobs_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Rows per page. Default 50, maximum 500.","default":50,"title":"Limit"},"description":"Rows per page. Default 50, maximum 500."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page.","title":"Cursor"},"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_JobResp_"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/chat":{"post":{"tags":["chat"],"summary":"Chat","operationId":"v1_chat_v1_corpora__corpus_id__chat_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving is unavailable; retry after the named delay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServingUnavailableDetail"}}}}}}},"/v1/corpora/{corpus_id}/chat/stream":{"post":{"tags":["chat"],"summary":"Chat Stream","description":"Token-streaming chat (SSE): head event with sources, delta events as the model writes, then a\ndone event with measured metrics. Tenant-scoped + JWT-authed exactly like /chat. Needs a ready\ncorpus on the vLLM backend (the streaming serve path); the HF path has no token stream.","operationId":"v1_chat_stream_v1_corpora__corpus_id__chat_stream_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving is unavailable; retry after the named delay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServingUnavailableDetail"}}}}}}},"/v1/mcp/{corpus_id}/query":{"post":{"tags":["chat"],"summary":"Mcp Query","description":"The answer route behind the hosted MCP server's query_corpus tool. Auth is a tenant API key\nrather than a user JWT, so an agent can ask questions without a user session. It differs from\n/corpora/{id}/chat in its source attribution: the ids it returns are the PINNABLE cart ids the\ninventory route below speaks, which is what lets a follow-up pin the same evidence.","operationId":"v1_mcp_query_v1_mcp__corpus_id__query_post","parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving is unavailable; retry after the named delay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServingUnavailableDetail"}}}}}}},"/v1/mcp/{corpus_id}/documents":{"get":{"tags":["chat"],"summary":"Mcp Documents","description":"The inventory behind the hosted MCP server's list_documents tool. `q` is an optional\ncase-insensitive substring matched against filename OR description; `total` is the count AFTER that\nfilter so the client can page it. DB-only (no storage round-trip); doc ids are the SAME pinnable\ncurrency as /query's used_docs/doc_ids.","operationId":"v1_mcp_documents_v1_mcp__corpus_id__documents_get","parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpDocsResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/sources":{"post":{"tags":["sources"],"summary":"Create Source","description":"Register a bucket this document base pulls from. Always 201; the `status` says whether it\nworks yet.\n\nREGISTRATION CANNOT REQUIRE A WORKING ROLE, because of the order the customer has to do things\nin. The trust policy they apply names an external id, and the external id comes from us — so the\nonly sequence that can succeed is: get an id, render the policy from it, apply it, register.\nValidating at registration and refusing on failure made the FIRST call in that sequence the one\nthat had to already work, which it cannot. Two changes fix it, and both are here:\n\n  * the caller may supply the `external_id` they already built their policy around (checked for\n    shape and for collision), and we mint one only when they have not;\n  * a failed AssumeRole is no longer fatal. The source is kept at `pending_validation` with the\n    plain reason on it, and the response carries the external id and the CloudFormation and\n    Terraform to apply — so a customer who registered first still has everything they need in\n    front of them. `POST .../validate` (or their first sync) flips it to `ready`.\n\nWhat stays fatal is anything the CUSTOMER got wrong in the request itself: a malformed bucket\nname or role ARN, a cross-region bucket they did not opt into, a duplicate prefix, an external id\nthat is too short. Those are 422s and 409s, because no amount of waiting fixes them.\n\nA `google_drive` or `sharepoint` source (CAAS-606) takes the other branch below: there is no role\nto assume and no bucket to list, so what gets checked is that the named connection is this\ntenant's, and the first sync is what proves the folder is readable.","operationId":"v1_create_source_v1_corpora__corpus_id__sources_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceCreateReq"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceCreateResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"get":{"tags":["sources"],"summary":"List Sources","description":"The sources feeding this document base. A read, so any valid credential for the tenant may\nmake it — scopes limit what a key can change or spend, not what it can see of its own workspace.","operationId":"v1_list_sources_v1_corpora__corpus_id__sources_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Rows per page. Default 50, maximum 500.","default":50,"title":"Limit"},"description":"Rows per page. Default 50, maximum 500."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page.","title":"Cursor"},"description":"Opaque cursor from the previous page's `next_cursor`. Omit for the first page."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_SourceResp_"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/sources/{source_id}":{"put":{"tags":["sources"],"summary":"Update Source","description":"Change a registered source in place: how it pulls, how often, and which role it uses.\n\nThe bucket and the prefix are not here, because they are what a customer's trust policy was\nwritten around and changing them in place would silently repoint a standing grant. That is a\nnew registration rather than an edit. Everything else is a setting people genuinely revise:\nthe mode, the schedule, the S3 Inventory destination (CAAS-604), and the credential.\n\nTHE CREDENTIAL IS THE POINT OF THIS ROUTE. A role ARN typed wrong used to leave a source that\ncould never sync and could never be repaired, only deleted and re-registered, which meant\nre-applying the IAM template over a typo. Changing the role or the external id re-checks access\nstraight away and reports the verdict.\n\nOmitted fields are left alone, so a client sends only what it is changing, and `model_fields_set`\nis what separates \"not mentioned\" from \"cleared\" — sending `schedule_minutes` as null turns the\nschedule off, while leaving it out keeps it. Sending both inventory fields empty turns inventory\noff and the next sync goes back to listing, which always works.\n\nThe access verdict is returned but not fatal, for the same reason registration is not: the\ncustomer may be applying the grant and editing the source in either order, and the first\ninventory report takes up to 48 hours to arrive. The templates come back whenever there is\nstill something to apply. A cross-region bucket is the one exception, as it is at registration:\nthat refusal is about cost, and waiting never fixes it.","operationId":"v1_update_source_v1_corpora__corpus_id__sources__source_id__put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceUpdateReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceCreateResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"delete":{"tags":["sources"],"summary":"Delete Source","description":"Stop pulling from a bucket.\n\nA hard delete of the source and its object rows, not a tombstone, and that is the right call for\nthis table: the rows exist only to answer \"what have we already fetched from this bucket\", a\nquestion that stops being meaningful the moment we stop fetching. Documents already ingested are\nuntouched — removing a source ends future syncs and does not retract what a past one delivered.\nThe cached role session is dropped too, so credentials for a bucket we no longer serve do not sit\nin process memory for the rest of the hour.\n\nPLAT-29: the documents this source fed have their `source_id` NULLED rather than deleted, so they\nmove under the Documents tab's \"Website uploads\" card instead of vanishing with the card that was\nshowing them. That is the same promise the paragraph above makes, made good in the one place a\ngrouped view could have broken it: the documents are still there, and now they are still visible.","operationId":"v1_delete_source_v1_corpora__corpus_id__sources__source_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"204":{"description":"Successful Response"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/sources/{source_id}/manifest.csv":{"get":{"tags":["sources"],"summary":"Export Source Manifest","description":"Download a CSV of the documents THIS source has fed into the base (PLAT-29).\n\nOne row per file, with the state it is in, why it is in that state, when it last moved and the\nhash of its text. It replaces scrolling a list of every document in the base: the question people\nactually had was \"what did this folder give me and is any of it broken\", which is a source-shaped\nquestion and a spreadsheet-shaped answer.\n\nIts twin is GET /corpora/{id}/documents/manifest.csv?source=upload, the same columns for the\ndocuments nobody pulled. Both build their rows with `corpora.document_manifest_rows`, so the two\nfiles always agree about what a column means.\n\nA read, so any valid credential for the tenant may make it — scopes limit what a key can change\nor spend, not what it can see of its own workspace (the same rule the sources list follows).","operationId":"v1_export_source_manifest_v1_corpora__corpus_id__sources__source_id__manifest_csv_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/sources/{source_id}/validate":{"post":{"tags":["sources"],"summary":"Validate Source","description":"Re-check a source: assume the role, list one page, record the verdict.\n\nThe one people actually use. A source goes to `error` when a customer rotates a policy or renames\na bucket, and this is how they confirm the fix without waiting for a sync. Unlike registration a\nfailure here does NOT delete the row: the source already existed and the customer may well be\nmid-repair, so the row stays with `error` and the reason on it.","operationId":"v1_validate_source_v1_corpora__corpus_id__sources__source_id__validate_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceValidationResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/sources/{source_id}/sync":{"post":{"tags":["sources"],"summary":"Sync Source","description":"Pull from this source now, and return the run to watch.\n\nThe request does the cheap half and returns: for S3 it computes the diff (a listing, no GETs) and\nqueues one download job per new or changed key; for Drive and SharePoint it queues the delta walk.\nNothing downloads inside this call, so a fifty-thousand-object bucket answers as fast as an empty\none.\n\n409 when a run for this source is already going. Two concurrent walks of one bucket would fetch\nthe same keys twice and race each other's `source_objects` writes, and the second one adds\nnothing — whatever it would find, the first is already finding.\n\n409 as well on a source that has never validated. It is registered but the trust policy is not\nworking yet (registration no longer refuses that — see create_source), and starting a run that\ncould only fail would put a failed run in the customer's history for a problem that is not a run\nproblem. The sync RETRIES the validation first, though, so a customer who applied their policy\nafter registering just clicks sync and it works — which is the \"or the first sync\" half of the\nflow, without them having to know there was a separate step.","operationId":"v1_sync_source_v1_corpora__corpus_id__sources__source_id__sync_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncRunResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/sources/s3/setup":{"get":{"tags":["sources"],"summary":"S3 Setup","description":"The IAM setup for one source, as CloudFormation and as Terraform.\n\nTakes the external id as a parameter rather than reading it from a source row, because the useful\norder is backwards from the obvious one: a customer wants the templates BEFORE the source exists,\napplies them, and registers with the role ARN that comes out. The endpoint is a pure renderer\nover the values it is given, so it works either way round.\n\nThat also means it holds no secret to leak. The external id is the caller's own input, and the\noutput is a policy document computed from it — supplying someone else's external id renders a\ntemplate that grants access to a role only they can create in an account only they control.","operationId":"v1_s3_setup_v1_sources_s3_setup_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"external_id","in":"query","required":true,"schema":{"type":"string","title":"External Id"}},{"name":"bucket","in":"query","required":true,"schema":{"type":"string","title":"Bucket"}},{"name":"prefix","in":"query","required":false,"schema":{"type":"string","default":"","title":"Prefix"}},{"name":"kms_key_arn","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kms Key Arn"}},{"name":"inventory_bucket","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inventory Bucket"}},{"name":"inventory_prefix","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inventory Prefix"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceSetupResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/corpora/{corpus_id}/upload-credentials":{"post":{"tags":["sources"],"summary":"Upload Credentials","description":"An hour of S3 credentials scoped to this document base's prefix, and the command to use them.\n\nThis is what makes \"push into Engram\" work with tools we did not write. Any S3 client speaks\nthese credentials, so a customer can use `aws s3 sync`, DataSync, or bucket replication without\nEngram being in the data path at all — the API task never holds a byte of the file.\n\nAn hour is the ceiling, not a preference: the task is already running under an assumed role, and\nSTS caps a chained session at one hour. Long enough for a large sync, short enough that a leaked\nset stops working the same afternoon.","operationId":"v1_upload_credentials_v1_corpora__corpus_id__upload_credentials_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"corpus_id","in":"path","required":true,"schema":{"type":"string","title":"Corpus Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadCredentialsResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/platform/info":{"get":{"tags":["sources"],"summary":"Platform Info","description":"Where Engram runs, and what that means for your bill.\n\nPublic, and deliberately so: the question \"which region should my bucket be in\" is one people\nhave before they have an account, and answering it behind a login guarantees somebody creates the\nbucket in the wrong place first. Deployment facts only — nothing about tenants, volumes, or\ncapacity.\n\nServed from config rather than written down anywhere, so the region a customer reads and the\nregion the platform actually runs in cannot disagree.","operationId":"v1_platform_info_v1_platform_info_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformInfoResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/plans":{"get":{"tags":["plans"],"summary":"List Plans","description":"The published plan matrix. No auth: this is a pricing page.","operationId":"v1_list_plans_v1_plans_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlansResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/billing/status":{"get":{"tags":["billing"],"summary":"Billing Status","description":"Billing status for the caller's workspace — ALWAYS 200, safe when billing is disabled.\nSurfaces the flag, the rate card (so pricing is visible in one place), whether the\nmanage-billing portal is available, and the full plan state: which tier, which term, the Stripe\nsubscription status we mirror, when a trial runs out, when the paid period renews, whether a\ncancellation is already pending at that renewal, whether Checkout has to happen first, and the\nresolved caps the entitlement checks enforce.","operationId":"v1_billing_status_v1_billing_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingStatusResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/billing/checkout":{"post":{"tags":["billing"],"summary":"Billing Checkout","description":"Start a Stripe Checkout Session for a paid tier and hand back the hosted URL.\n\nSubscription mode, card ALWAYS required. `payment_method_collection=\"always\"` is the line that\nmakes the card-required trial real: without it Stripe lets a customer start a trial with no\npayment method, and day 15 is then a dead subscription instead of a conversion.\n`trial_period_days` is only passed while the tenant is actually on trial, so an upgrade from a\npaid plan never grants a second free run.\n\nENTERPRISE / INVOICE BILLING. A tier that carries the invoice_billing feature is collected by\nINVOICE on net terms, so a finance team pays the way it already pays everyone, self-serve, with\nnobody at Engram in the loop. TERMS are the benefit the plan sells, and terms alone (PLAT-20):\npaying from a US bank account is a payment method the account switches on for everybody, so the\nplan copy no longer sells it as a higher tier. Invoice collection needs the dashboard settings\nthe setup script lists at the end of a run; until those are on, Stripe rejects the session\nrather than silently downgrading it, which is the failure we want.\n\nSTRIPE TAX rides behind STRIPE_TAX_ENABLED for the same reason: it has to be configured in the\ndashboard first, so the flag and the setting flip together.\n\nONE SUBSCRIPTION PER WORKSPACE (BILL-4). Checkout MINTS a subscription, so a workspace that\nalready has a live one gets a 409 pointing at change-plan instead — the first live UAT checkout\nran twice and left the customer paying for two. The id is what proves a subscription exists:\nopen signup stamps free/active on a workspace that has never been to Stripe at all, so status\nalone would refuse every genuine first purchase. Until the webhook stamps that id there is\nnothing to be double-billed against, which is exactly when a retry should be allowed through.\n\nCURRENT LEGAL ACCEPTANCE IS REQUIRED TO BUY (require_all_current_legal): a workspace that has not\naccepted every published document gets a 403 legal_acceptance_required naming the first one and\nthe page to accept it on, because a subscription is sold under those terms. It is a dependency,\nso it answers before the body — including before the billing-disabled 503.","operationId":"v1_billing_checkout_v1_billing_checkout_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/billing/change-plan":{"post":{"tags":["billing"],"summary":"Billing Change Plan","description":"Move an existing subscription between tiers. Two different Stripe mechanisms, because the\nproduct rule is two different things:\n\n  UPGRADE — immediate, with proration. Subscription.modify swaps the price on the existing item\n    and `proration_behavior=\"create_prorations\"` bills the difference for the rest of the\n    period. The customer asked for more capacity; they get it on this request, and limits.py\n    reads the new cap the moment the webhook lands (or the stamp below, whichever is first).\n\n  DOWNGRADE — honored at the end of the period they already paid for. That is a subscription\n    SCHEDULE, not a modify: create a schedule from the live subscription and append a phase on\n    the new price. Stripe switches at the renewal boundary and releases the schedule afterwards,\n    so nobody loses capacity they bought and nobody is credited for time they used.\n\nA tenant with no subscription is sent to Checkout instead — there is nothing to change yet.\n\nCURRENT LEGAL ACCEPTANCE IS REQUIRED TO CHANGE A PAID PLAN (require_all_current_legal), the same\n403 the checkout route raises: moving between paid tiers is buying. The billing PORTAL stays\nungated, so an existing customer can always manage or cancel what they already bought.","operationId":"v1_billing_change_plan_v1_billing_change_plan_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/billing/portal":{"post":{"tags":["billing"],"summary":"Billing Portal","description":"Open the Stripe billing portal for the caller's workspace — cards, invoices, plan changes and\ncancellation, all handled by Stripe so none of it needs us. 503 while billing is disabled (the\nbeta). When enabled: lazily create the Stripe customer for the tenant if it has none yet\n(stamped with tenant_id metadata + persisted), then create a billing-portal session.","operationId":"v1_billing_portal_v1_billing_portal_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingPortalResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/billing/webhook":{"post":{"tags":["billing"],"summary":"Billing Webhook","description":"Stripe -> control-plane webhook. NO auth dependency: Stripe calls it, and authenticity is proven\nby the signature (verified with stripe.Webhook.construct_event against STRIPE_WEBHOOK_SECRET), not a\nuser JWT. 503 while billing is disabled; 400 on a bad/absent signature.\n\nEvery event is acknowledged 200 once the signature verifies, including ones we do nothing with:\na non-200 makes Stripe retry, and retrying an event we deliberately ignore is noise. A handler\nthat throws is logged and still acked for the same reason — Stripe cannot fix our bug by\nsending it again.","operationId":"v1_billing_webhook_v1_billing_webhook_post","parameters":[{"name":"stripe-signature","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stripe-Signature"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/status":{"get":{"tags":["status"],"summary":"Status","description":"Live platform status for signed-in users (any workspace member or API key), cached\nSTATUS_CACHE_S seconds. It was public until 2026-09-11; the founder chose not to advertise\noutages to the world, so the sign-in footer link went and the route now needs a session.","operationId":"v1_status_v1_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/status/slo":{"get":{"tags":["status"],"summary":"Status Slo","description":"The service objectives on their own — no probes, no live state, just the numbers and the\nwords. Separate from /status so the pricing and terms pages can quote them without pulling a\nhealth check, and so a change to the objectives is one fetch to verify.","operationId":"v1_status_slo_v1_status_slo_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response V1 Status Slo V1 Status Slo Get"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/audit":{"get":{"tags":["audit"],"summary":"List Audit Events","description":"The caller's tenant's audit events, newest first. `limit` defaults to 100 (max 1000). Filtered\nstrictly on the JWT's tenant_id — a tenant never sees another tenant's (or the _system GC) receipts.","operationId":"v1_list_audit_events_v1_audit_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_AuditEventResp_"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/audit/deletions":{"get":{"tags":["audit"],"summary":"List Deletion Receipts","description":"The tenant's DELETION receipts, newest first (CAAS-1304).\n\nWHY a route of its own rather than a filter on /audit: a deletion receipt is the one audit row a\ncustomer is entitled to go and find after the fact, and the obvious place to look for it —\nGET /corpora/{id}/deletion-receipts — is impossible by construction, because the corpus whose id\nyou would use is exactly what the delete removed. So the receipts are addressable at the TENANT\nlevel instead. Each row's `detail` carries the per-tier outcome (see routers/corpora.py), whose\ntier keys line up with GET /platform/erasure-timeline.\n\nTenant-ADMIN rather than any member: the receipts name every document base the workspace has ever\ndeleted, which is a workspace-wide history, not the caller's own activity.","operationId":"v1_list_deletion_receipts_v1_audit_deletions_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_AuditEventResp_"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/audit/export":{"get":{"tags":["audit"],"summary":"Export Audit","description":"Download this workspace's audit log.\n\nThe point of the feature: a compliance reviewer asks \"show me everything that happened to this\ndata\", and the answer is a file, not a screenshot of a table. Oldest first, because a log reads\nforward. `jsonl` keeps `detail` as the JSON it is, one event per line, which is what a pipeline\nwants; `csv` is what a spreadsheet wants.\n\nTenant-scoped by construction — the tenant comes from the credential, never from a parameter, so\nthere is no id here for anyone to change.","operationId":"v1_export_audit_v1_audit_export_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","pattern":"^(csv|jsonl|ndjson|json)$","default":"jsonl","title":"Format"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date or timestamp, inclusive","title":"Since"},"description":"ISO date or timestamp, inclusive"},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date or timestamp, exclusive","title":"Until"},"description":"ISO date or timestamp, exclusive"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/enterprise/sso":{"get":{"tags":["enterprise"],"summary":"Get Sso","description":"The workspace's SSO configuration, secret-free.\n\nNOT plan-gated: an admin on any plan can look at this page and see the redirect URI they would\nneed. The gate is on writing, which is where the feature actually costs something.","operationId":"v1_get_sso_v1_enterprise_sso_get","security":[{"OAuth2PasswordBearer":[]}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoConfigResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"put":{"tags":["enterprise"],"summary":"Put Sso","description":"Configure single sign-on for this workspace.\n\nValidated against the real provider before anything is stored: we fetch the issuer's discovery\ndocument and check it names the same issuer and carries the endpoints the flow needs. An admin\nwho mistypes the issuer finds out here, on the screen where they typed it, instead of at their\nfirst employee's first login.\n\nDomain uniqueness is enforced across the whole deployment. POST /auth/sso/start maps an address\nto a workspace through this list, so a domain claimed twice would have two answers and we would\nbe choosing which company's identity provider gets to vouch for a person. Refuse instead.","operationId":"v1_put_sso_v1_enterprise_sso_put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoConfigReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoConfigResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"delete":{"tags":["enterprise"],"summary":"Delete Sso","description":"Turn single sign-on off and forget the configuration, secret included.\n\nNOT plan-gated, deliberately: a workspace that drops to a plan without SSO has to be able to\nturn it off, and enforcement is what would otherwise lock everyone out. Turning a security\nfeature OFF is never the thing to charge for.","operationId":"v1_delete_sso_v1_enterprise_sso_delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoConfigResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/enterprise/sso/test":{"post":{"tags":["enterprise"],"summary":"Test Sso","description":"Hand the admin an authorization URL they can open in a new tab.\n\nThis is the \"does it actually work\" button. It builds the same URL a real sign-in would, against\nthe same cached client, so a wrong client id or an unregistered redirect URI shows up as the\nprovider's own error message on the provider's own page, which is the clearest possible place\nfor it to appear.","operationId":"v1_test_sso_v1_enterprise_sso_test_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoTestResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/enterprise/ip-allowlist":{"get":{"tags":["enterprise"],"summary":"Get Ip Allowlist","description":"The stored list, plus the address this request came from so the page can offer it as the\nfirst entry. Not plan-gated for the same reason as GET /enterprise/sso.","operationId":"v1_get_ip_allowlist_v1_enterprise_ip_allowlist_get","security":[{"OAuth2PasswordBearer":[]}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IpAllowlistResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"put":{"tags":["enterprise"],"summary":"Put Ip Allowlist","description":"Restrict this workspace to a set of networks.\n\nThe lockout guard is the whole reason this route is more than a setter. An admin who saves a\nlist their own address falls outside has just locked their company out of its own account, and\nthe next request, including the one that would undo it, is refused. So the write refuses first\nand explains what it saw. `confirm_lockout=true` is the deliberate override, because the case is\nreal: an admin at home configuring the office range means it.\n\nAn empty list clears the restriction and is always allowed.","operationId":"v1_put_ip_allowlist_v1_enterprise_ip_allowlist_put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IpAllowlistReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IpAllowlistResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/enterprise/kms-key":{"get":{"tags":["enterprise"],"summary":"Get Kms Key","operationId":"v1_get_kms_key_v1_enterprise_kms_key_get","security":[{"OAuth2PasswordBearer":[]}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KmsKeyResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}},"put":{"tags":["enterprise"],"summary":"Put Kms Key","description":"Point this workspace's documents at a key the customer owns.\n\nGated on `kms_key`, which is ENTERPRISE ONLY. It used to borrow a broader flag, which also\nlet Business in — but Enterprise's pricing copy sells the customer key as something Business\ndoes not have (\"Everything in Business plus IP allowlists on each workspace and your own KMS\nkey on your documents\"), so the borrowed flag was refusing on the wrong line. FEAT-1 gives the\nkey its own flag and puts it on the tier the copy actually sells it on.\n\nSETTING A KEY IS GATED, CLEARING ONE IS NOT — which is why the check is inside the handler on\n`check_plan_feature` rather than on the route through `require_plan_feature`. The tier moved,\nso there are workspaces on Business that set a key back when Business carried the feature; if\nthe whole route were gated, those customers could neither keep managing the key nor take it\noff, which is a trap of our making. Clearing is the way out and it stays open on every tier.\nThe auth level is unchanged either way: tenant admin, same as every other route here.\n\nDocuments already written with a customer key keep being decrypted with it whatever plan the\nworkspace is on, because S3 reads the key off the object; nothing on this route touches stored\ndata. A downgrade that made a customer's own documents unreadable would be a far worse outcome\nthan an unenforced promise.\n\nClearing the key returns the workspace to the bucket's default encryption: existing objects\nkeep the key they were written with, and new writes use the default, which is the honest\nbehaviour of S3 and worth saying out loud.","operationId":"v1_put_kms_key_v1_enterprise_kms_key_put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KmsKeyReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KmsKeyResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/usage/export":{"get":{"tags":["enterprise"],"summary":"Export Usage","description":"Download this workspace's usage for a month.\n\nSame plan flag as the audit export: a finance or compliance team that needs one almost always\nneeds the other, and splitting them would add a plan row nobody asked for.","operationId":"v1_export_usage_v1_usage_export_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","pattern":"^(csv|jsonl|ndjson|json)$","default":"csv","title":"Format"}},{"name":"month","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"YYYY-MM; defaults to the current month","title":"Month"},"description":"YYYY-MM; defaults to the current month"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/legal/status":{"get":{"tags":["legal"],"summary":"Legal Status","description":"Which current versions this workspace has accepted.\n\nAny member, not just an admin: everyone in a workspace can reasonably want to know whether the\nterms are signed, and this returns no personal data beyond the name of whoever clicked.\n\n`all_current` is the one flag a client needs — it is what the checkout gate and the \"please\naccept the updated terms\" banner both key off, so neither has to re-derive it.","operationId":"v1_legal_status_v1_legal_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalStatusResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/legal/{document}":{"get":{"tags":["legal"],"summary":"Get Legal Document","description":"The current text of a document. PUBLIC: someone has to be able to read the terms before they\nhave an account, and a legal document that needs a login to read is not much of a legal\ndocument.","operationId":"v1_get_legal_document_v1_legal__document__get","parameters":[{"name":"document","in":"path","required":true,"schema":{"type":"string","title":"Document"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalDocResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/legal/{document}/receipt":{"get":{"tags":["legal"],"summary":"Get Legal Receipt","description":"The acceptance receipt for this workspace: the accepted text, plus who accepted it, when, and\nfrom where.\n\nThis is what a downloadable signed copy would have been. A PDF is out of scope for this tranche,\nand a JSON receipt carrying the exact text is the more useful artifact anyway: it is the same\nfacts, it diffs, and a client can render or print it however it likes.\n\nTenant ADMIN rather than any member, matching /audit/deletions: the receipt names an individual\nand the address they acted from, which is workspace evidence rather than general reading.","operationId":"v1_get_legal_receipt_v1_legal__document__receipt_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"document","in":"path","required":true,"schema":{"type":"string","title":"Document"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalReceiptResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/legal/accept":{"post":{"tags":["legal"],"summary":"Accept Legal","description":"Accept a document on behalf of the workspace.\n\nTENANT ADMIN, because accepting binds the whole workspace and a member cannot bind their\nemployer. The version in the request must match the published one: a page that has been open\nsince before a version bump must not be able to accept the new text by accident, so a stale\nversion is refused with what the current one is.\n\nIdempotent. A double-clicked button, or a second admin clicking the same thing, returns the\nacceptance that already exists rather than writing a duplicate row a later audit would have to\nreconcile — the unique index on (tenant, document, version) enforces the same thing in the\ndatabase.","operationId":"v1_accept_legal_v1_legal_accept_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours.","title":"Idempotency-Key"},"description":"Optional. Retry a write with the same key to get the first response back instead of doing the work twice. Records are kept for 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalAcceptReq"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalDocStatusResp"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}}}},"/v1/platform/erasure-timeline":{"get":{"tags":["platform"],"summary":"Get Erasure Timeline","description":"What happens to every copy of your data when you delete it, and how long each copy can live.\nGenerated from the constants this module enforces against, so it can never drift from the\nlifecycle rules and TTLs actually configured.","operationId":"v1_get_erasure_timeline_v1_platform_erasure_timeline_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ErasureStageResp"},"type":"array","title":"Response V1 Get Erasure Timeline V1 Platform Erasure Timeline Get"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"401":{"description":"Missing or rejected credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"403":{"description":"Not allowed for this credential or plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"429":{"description":"Rate limited or over a plan cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}},"503":{"description":"Serving temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResp"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1":{"get":{"tags":["contract"],"summary":"Version document","description":"What this version is, and what in it is on its way out.\n\nA client polls this to learn whether to worry. `deprecations` is empty and will stay empty until\nsomething in v1 is actually scheduled for removal — at which point the affected routes also start\nanswering with a `Deprecation` header and a date, so a client learns it from the call it is\nalready making rather than from a changelog nobody reads.","operationId":"api_version_v1_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiVersionResp"}}}}}}},"/metrics":{"get":{"summary":"Prometheus Metrics","description":"Prometheus scrape endpoint. NOT JWT-authed (a scraper has no user session); guarded instead by\nthe shared METRICS_TOKEN bearer when set, open otherwise (local dev / already-isolated VPC).","operationId":"prometheus_metrics_metrics_get","parameters":[{"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"}}}}}}},"/":{"get":{"tags":["contract"],"summary":"Service and API versions","description":"Version discovery: which contracts this deployment serves (CAAS-802).\n\nA NEW route, not a change to /health. /health is the ALB's target check and its body is asserted\nbyte for byte by the suite, so the version advertisement lives at the one path that had nothing\non it. A client reads `api_versions` here and picks its base path from it — which is exactly what\nthe CLI's `--api-version auto` does.","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiRootResp"}}}}}}},"/health":{"get":{"summary":"Health","description":"Liveness: the process is up.","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/ready":{"get":{"summary":"Ready","description":"Readiness: dependencies (DB, ML service) are reachable. 503 if not, so an\norchestrator (ECS/K8s) holds traffic until the app can actually serve.","operationId":"ready_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AcceptInviteReq":{"properties":{"token":{"type":"string","maxLength":512,"minLength":1,"title":"Token"},"password":{"type":"string","maxLength":128,"minLength":8,"title":"Password"},"name":{"type":"string","maxLength":120,"minLength":1,"title":"Name"}},"type":"object","required":["token","password","name"],"title":"AcceptInviteReq","description":"Redeem an invite/approval link: set a password and activate the account.\n`name` is required — it's the member's display name across the workspace."},"AnswerFeedbackReq":{"properties":{"rating":{"type":"string","enum":["accurate","partial","inaccurate"],"title":"Rating"},"question":{"type":"string","maxLength":2000,"title":"Question"},"answer_excerpt":{"type":"string","maxLength":2000,"title":"Answer Excerpt"}},"type":"object","required":["rating","question","answer_excerpt"],"title":"AnswerFeedbackReq","description":"POST /corpora/{id}/feedback body — a 'how accurate was this answer' rating. question /\nanswer_excerpt are bounded so a client can't persist an unbounded blob (the excerpt is a snippet,\nnot the full transcript)."},"ApiError":{"properties":{"code":{"type":"string","title":"Code","description":"Stable machine-readable code. Switch on this, never on `message`."},"message":{"type":"string","title":"Message","description":"One line, written to be shown to a person or read by a model."},"status":{"type":"integer","title":"Status","description":"The HTTP status, repeated in the body so a log line is complete."},"details":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Details","description":"The structured extras a refusal carried (a cap, a retry_after, an upgrade_url, the per-field validation errors). Absent when there are none."},"request_id":{"type":"string","title":"Request Id","description":"Echoed in the X-Request-Id header. Quote it to support."}},"type":"object","required":["code","message","status","request_id"],"title":"ApiError","description":"The inside of every failure under /v1. See app/api_errors.py for the mapping from the three\nlegacy `detail` shapes onto this one."},"ApiErrorResp":{"properties":{"error":{"$ref":"#/components/schemas/ApiError"}},"type":"object","required":["error"],"title":"ApiErrorResp","description":"The whole failure body: `{\"error\": {...}}`. One key, so a client can tell a failure from a\nsuccess by shape alone."},"ApiKeyCreateReq":{"properties":{"name":{"type":"string","maxLength":120,"minLength":1,"title":"Name"},"scopes":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Scopes"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["name","scopes"],"title":"ApiKeyCreateReq","description":"Mint a key. `scopes` must be a non-empty subset of security.API_SCOPES — an unknown value is\na 422 rather than a silently-dropped scope, so a typo'd `\"quary\"` fails at the call that made it\ninstead of surfacing later as a mystery 403."},"ApiKeyCreatedResp":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"prefix":{"type":"string","title":"Prefix"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","default":[]},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"key":{"type":"string","title":"Key"}},"type":"object","required":["id","name","prefix","created_at","key"],"title":"ApiKeyCreatedResp","description":"The create response, and the ONLY place the secret ever appears — it is not stored, so it\ncannot be shown again. The separate type (rather than an optional field on ApiKeyResp) is what\nmakes that a compile-time-ish guarantee instead of a convention."},"ApiKeyResp":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"prefix":{"type":"string","title":"Prefix"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","default":[]},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","prefix","created_at"],"title":"ApiKeyResp","description":"A key's METADATA. Deliberately has no secret field: this shape is what list returns, so the\nsecret cannot leak through it even by accident."},"ApiRootResp":{"properties":{"service":{"type":"string","title":"Service"},"api_versions":{"items":{"type":"string"},"type":"array","title":"Api Versions"},"docs":{"type":"string","title":"Docs"}},"type":"object","required":["service","api_versions","docs"],"title":"ApiRootResp","description":"GET / — the entry point a client uses to discover which versions this deployment serves."},"ApiVersionResp":{"properties":{"version":{"type":"string","title":"Version"},"deprecations":{"items":{"type":"string"},"type":"array","title":"Deprecations","description":"Parts of this version scheduled for removal. Empty means nothing is."},"docs":{"type":"string","title":"Docs"}},"type":"object","required":["version","docs"],"title":"ApiVersionResp","description":"GET /v1 — what this version is and what is on its way out."},"AuditEventResp":{"properties":{"id":{"type":"string","title":"Id"},"event":{"type":"string","title":"Event"},"corpus_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Corpus Id"},"detail":{"type":"string","title":"Detail"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","event","detail","created_at"],"title":"AuditEventResp"},"BillingPortalResp":{"properties":{"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"BillingPortalResp","description":"POST /billing/portal — the Stripe billing-portal session URL to redirect the admin to."},"BillingResp":{"properties":{"plan":{"type":"string","title":"Plan","default":"beta"},"limits":{"additionalProperties":true,"type":"object","title":"Limits","default":{}},"usage":{"additionalProperties":true,"type":"object","title":"Usage","default":{}},"rate_card":{"additionalProperties":true,"type":"object","title":"Rate Card","default":{}},"estimated_cost_usd":{"type":"number","title":"Estimated Cost Usd","default":0.0},"currency":{"type":"string","title":"Currency","default":"usd"},"period":{"type":"string","title":"Period"}},"type":"object","required":["period"],"title":"BillingResp","description":"GET /admin/billing — the billing shell (Stripe deferred). Plan + limits + current\nusage + an estimated cost from the pricing rate card."},"BillingStatusResp":{"properties":{"enabled":{"type":"boolean","title":"Enabled","default":false},"livemode":{"type":"boolean","title":"Livemode","default":false},"rate_card":{"additionalProperties":true,"type":"object","title":"Rate Card","default":{}},"portal_available":{"type":"boolean","title":"Portal Available","default":false},"plan":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan"},"plan_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan Name"},"interval":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Interval"},"subscription_status":{"type":"string","title":"Subscription Status","default":"none"},"trial_ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Trial Ends At"},"current_period_end":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Current Period End"},"cancel_at_period_end":{"type":"boolean","title":"Cancel At Period End","default":false},"scheduled_change":{"anyOf":[{"$ref":"#/components/schemas/ScheduledPlanChange"},{"type":"null"}]},"next_invoice":{"anyOf":[{"$ref":"#/components/schemas/NextInvoice"},{"type":"null"}]},"checkout_required":{"type":"boolean","title":"Checkout Required","default":false},"entitlements":{"additionalProperties":true,"type":"object","title":"Entitlements","default":{}}},"type":"object","title":"BillingStatusResp","description":"GET /billing/status — always 200, safe when billing is disabled. `enabled` mirrors the flag;\n`rate_card` is the pricing rate card; `portal_available` tells the UI whether to offer the\n'manage billing' button. Everything from `plan` down is the CAAS-1402 plan state, all additive\nand all null/false for a legacy beta tenant, so the existing billing shell keeps rendering."},"Body_login_auth_login_post":{"properties":{"remember_me":{"type":"boolean","title":"Remember Me","default":false},"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_auth_login_post"},"Body_upload_documents_corpora__corpus_id__documents_post":{"properties":{"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_upload_documents_corpora__corpus_id__documents_post"},"Body_v1_login_v1_auth_login_post":{"properties":{"remember_me":{"type":"boolean","title":"Remember Me","default":false},"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_v1_login_v1_auth_login_post"},"Body_v1_upload_documents_v1_corpora__corpus_id__documents_post":{"properties":{"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_v1_upload_documents_v1_corpora__corpus_id__documents_post"},"BrowseFolderResp":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"}},"type":"object","required":["id","name"],"title":"BrowseFolderResp","description":"A folder the user can drill into. `id` is an opaque provider ref the client passes straight back\n(Drive file id / \"root\"; SharePoint \"site:<id>\" or \"item:<driveId>:<itemId>\")."},"BrowseResp":{"properties":{"folders":{"items":{"$ref":"#/components/schemas/BrowseFolderResp"},"type":"array","title":"Folders"},"supported_files":{"type":"integer","title":"Supported Files"},"path_hint":{"type":"string","title":"Path Hint"}},"type":"object","required":["folders","supported_files","path_hint"],"title":"BrowseResp"},"ChangePlanReq":{"properties":{"plan":{"type":"string","maxLength":32,"minLength":1,"title":"Plan"},"interval":{"type":"string","pattern":"^(monthly|annual)$","title":"Interval","default":"monthly"}},"type":"object","required":["plan"],"title":"ChangePlanReq","description":"POST /billing/change-plan — move an existing subscription between tiers. Upgrades apply\nimmediately with proration; downgrades are scheduled for the end of the paid period."},"ChangePlanResp":{"properties":{"plan":{"type":"string","title":"Plan"},"interval":{"type":"string","title":"Interval"},"effective":{"type":"string","title":"Effective"},"effective_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Effective At"},"message":{"type":"string","title":"Message","default":""}},"type":"object","required":["plan","interval","effective"],"title":"ChangePlanResp","description":"What actually happened, so the UI can say it plainly rather than guess."},"ChatMsg":{"properties":{"role":{"type":"string","pattern":"^(user|assistant)$","title":"Role"},"content":{"type":"string","maxLength":4000,"minLength":1,"title":"Content"}},"type":"object","required":["role","content"],"title":"ChatMsg"},"ChatReq":{"properties":{"question":{"type":"string","maxLength":4000,"minLength":1,"title":"Question"},"k":{"type":"integer","maximum":20.0,"minimum":1.0,"title":"K","default":3},"history":{"items":{"$ref":"#/components/schemas/ChatMsg"},"type":"array","maxItems":20,"title":"History"},"doc_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":20},{"type":"null"}],"title":"Doc Ids"},"max_tokens":{"anyOf":[{"type":"integer","maximum":4096.0,"minimum":1.0},{"type":"null"}],"title":"Max Tokens"},"mode":{"type":"string","enum":["cart","rag"],"title":"Mode","default":"cart"}},"type":"object","required":["question"],"title":"ChatReq"},"ChatResp":{"properties":{"documents_ready":{"type":"integer","title":"Documents Ready","default":0},"documents_onboarding":{"type":"integer","title":"Documents Onboarding","default":0},"documents_failed":{"type":"integer","title":"Documents Failed","default":0},"last_synced_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Synced At"},"answer":{"type":"string","title":"Answer"},"used_docs":{"items":{"type":"string"},"type":"array","title":"Used Docs"},"sources":{"items":{"$ref":"#/components/schemas/SourceRef"},"type":"array","title":"Sources","default":[]},"used_documents":{"items":{"$ref":"#/components/schemas/UsedDocument"},"type":"array","title":"Used Documents","default":[]},"condensed_q":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condensed Q"},"documents_warming":{"type":"integer","title":"Documents Warming","default":0}},"type":"object","required":["answer","used_docs"],"title":"ChatResp"},"CheckoutReq":{"properties":{"plan":{"type":"string","maxLength":32,"minLength":1,"title":"Plan"},"interval":{"type":"string","pattern":"^(monthly|annual)$","title":"Interval","default":"monthly"}},"type":"object","required":["plan"],"title":"CheckoutReq","description":"POST /billing/checkout — start a Stripe Checkout Session for a paid tier. `plan` must be a\npurchasable tier (the trial is not bought), `interval` picks which price id is used."},"CheckoutResp":{"properties":{"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"CheckoutResp","description":"The hosted Checkout URL to send the browser to."},"CommitReq":{"properties":{"files":{"items":{"$ref":"#/components/schemas/ManifestFile"},"type":"array","maxItems":10000,"title":"Files"},"onboard":{"type":"boolean","title":"Onboard","default":true},"delete_missing":{"type":"boolean","title":"Delete Missing","default":false}},"type":"object","title":"CommitReq","description":"Register uploaded files and start the work (CAAS-107).\n\n`onboard` False registers and parses without spending GPU time â€” a fifty-thousand-file initial\nload can land before anyone decides to build it. `delete_missing` is CAAS-109's mirror flag, and\nit defaults to False because deleting documents is the one thing a sync must never do by\naccident."},"CommitResp":{"properties":{"sync_run_id":{"type":"string","title":"Sync Run Id"},"documents":{"items":{"$ref":"#/components/schemas/CommittedDocument"},"type":"array","title":"Documents","default":[]},"removed":{"items":{"type":"string"},"type":"array","title":"Removed","default":[]}},"type":"object","required":["sync_run_id"],"title":"CommitResp"},"CommittedDocument":{"properties":{"path":{"type":"string","title":"Path"},"id":{"type":"string","title":"Id"},"lifecycle":{"type":"string","title":"Lifecycle"}},"type":"object","required":["path","id","lifecycle"],"title":"CommittedDocument"},"ComponentResp":{"properties":{"name":{"type":"string","title":"Name"},"state":{"type":"string","title":"State"},"detail":{"type":"string","title":"Detail"}},"type":"object","required":["name","state","detail"],"title":"ComponentResp","description":"One component's state, in words a customer understands. No hostname, no address, no id."},"ConnectionResp":{"properties":{"id":{"type":"string","title":"Id"},"provider":{"type":"string","title":"Provider"},"account_label":{"type":"string","title":"Account Label"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"shared_folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Shared Folder Id"}},"type":"object","required":["id","provider","account_label","created_at"],"title":"ConnectionResp","description":"A tenant's authorized source connection (no tokens ever exposed — only the account label)."},"ConnectorAuthorizeResp":{"properties":{"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"ConnectorAuthorizeResp","description":"The provider consent URL the SPA redirects itself to (we don't 302 the XHR)."},"CorpusCreateReq":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"source_type":{"type":"string","title":"Source Type","default":"upload"}},"type":"object","required":["name"],"title":"CorpusCreateReq"},"CorpusDescribeResp":{"properties":{"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","title":"CorpusDescribeResp","description":"POST /corpora/{id}/describe — the manual refresh/backfill surface for the corpus-level catalog\ndescription (the same blurb the post-onboard describe pass writes). Corpora onboarded BEFORE that\npass existed (or whose description the old token budget truncated) get a fresh one without a\nretrain. `description` stays None when the pass failed OR produced no complete sentence (the\ntruncation guard refuses to store a mid-sentence fragment — see jobs._write_corpus_description)."},"CorpusResp":{"properties":{"documents_ready":{"type":"integer","title":"Documents Ready","default":0},"documents_onboarding":{"type":"integer","title":"Documents Onboarding","default":0},"documents_failed":{"type":"integer","title":"Documents Failed","default":0},"last_synced_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Synced At"},"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"source_type":{"type":"string","title":"Source Type"},"status":{"type":"string","title":"Status"},"n_documents":{"type":"integer","title":"N Documents"},"onboarding_step":{"type":"string","title":"Onboarding Step","default":"name"},"model_tier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Tier"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"n_cartridges":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"N Cartridges"},"train_seconds":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Train Seconds"},"corpus_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Corpus Tokens"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","source_type","status","n_documents","created_at"],"title":"CorpusResp"},"CorpusUsageResp":{"properties":{"corpus_id":{"type":"string","title":"Corpus Id"},"name":{"type":"string","title":"Name"},"queries":{"type":"integer","title":"Queries","default":0},"documents":{"type":"integer","title":"Documents","default":0},"storage_gb":{"type":"number","title":"Storage Gb","default":0.0},"gpu_seconds":{"type":"number","title":"Gpu Seconds","default":0.0}},"type":"object","required":["corpus_id","name"],"title":"CorpusUsageResp"},"DiffResp":{"properties":{"new":{"items":{"type":"string"},"type":"array","title":"New","default":[]},"changed":{"items":{"type":"string"},"type":"array","title":"Changed","default":[]},"unchanged":{"items":{"type":"string"},"type":"array","title":"Unchanged","default":[]},"missing":{"items":{"type":"string"},"type":"array","title":"Missing","default":[]}},"type":"object","title":"DiffResp","description":"The rsync answer (CAAS-106). Four disjoint buckets of PATHS:\nnew       â€” we have no row for this path\nchanged   â€” we have a row and its raw hash differs (a legacy row with no raw hash lands here,\n            because \"we cannot prove it is the same\" has to mean \"send it\")\nunchanged â€” same path, same raw hash, nothing to do\nmissing   â€” we hold a document the manifest does not mention; mirror mode deletes these."},"DocumentResp":{"properties":{"id":{"type":"string","title":"Id"},"filename":{"type":"string","title":"Filename"},"size":{"type":"integer","title":"Size"},"parse_status":{"type":"string","title":"Parse Status","default":"pending"},"parse_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parse Error"},"onboard_status":{"type":"string","title":"Onboard Status","default":"pending"},"lifecycle":{"type":"string","title":"Lifecycle","default":"pending"},"lifecycle_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lifecycle Error"},"lifecycle_updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Lifecycle Updated At"},"content_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Hash"},"cart_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cart Id"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id"},"parent_document_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Document Id"},"section_index":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Section Index"},"section_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Section Title"},"section_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Section Count"}},"type":"object","required":["id","filename","size"],"title":"DocumentResp"},"ErasureStageResp":{"properties":{"tier":{"type":"string","title":"Tier"},"where":{"type":"string","title":"Where"},"what":{"type":"string","title":"What"},"when":{"type":"string","title":"When"},"max_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Days"}},"type":"object","required":["tier","where","what","when"],"title":"ErasureStageResp","description":"One line of the erasure timeline (app/erasure.py). `max_days` is the worst-case life of that\ncopy: 0 = gone in the delete call, null = kept on purpose (the audit receipt). `tier` matches the\ntier keys in a deletion receipt, so a receipt reads against this timeline line for line."},"ForgotPasswordReq":{"properties":{"email":{"type":"string","maxLength":254,"title":"Email"}},"type":"object","required":["email"],"title":"ForgotPasswordReq"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ImportItemReq":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"mime_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mime Type"}},"type":"object","required":["id","name"],"title":"ImportItemReq","description":"One item the user explicitly picked in the Google Picker (file OR folder). We already have its\nid/name/mime_type from the Picker, so the import needs NO listing call to classify it. mime_type is\noptional (a plain file's extension is enough); a folder is signalled by the folder mime type."},"ImportReq":{"properties":{"connection_id":{"type":"string","title":"Connection Id"},"folder_id":{"type":"string","title":"Folder Id","default":""},"folder_name":{"type":"string","maxLength":400,"title":"Folder Name","default":""},"site_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Id"},"items":{"anyOf":[{"items":{"$ref":"#/components/schemas/ImportItemReq"},"type":"array"},{"type":"null"}],"title":"Items"}},"type":"object","required":["connection_id"],"title":"ImportReq","description":"Start importing a connected source into the corpus.\n\nGoogle Drive: `items` carries the Google Picker multiselect picks. This matters because under the\ndrive.file scope each explicitly picked FILE is itself the access grant, while a picked FOLDER's\nchildren may not be granted at all (found live: folder-only imports walked to 0 added). So picked\nfiles must import DIRECTLY by id (guaranteed readable) and folders are walked only opportunistically.\nfolder_id stays as the legacy single-folder ref and as SharePoint's selection (SharePoint keeps\nusing folder_id + the in-app browse; it has no Picker).\n\nsite_id is only needed for a SharePoint site's default library (optional)."},"ImportStatusResp":{"properties":{"state":{"type":"string","title":"State"},"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id"},"folder_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Name"},"imported":{"type":"integer","title":"Imported","default":0},"skipped":{"type":"integer","title":"Skipped","default":0},"failed":{"type":"integer","title":"Failed","default":0},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"finished_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Finished At"}},"type":"object","required":["state"],"title":"ImportStatusResp","description":"Latest ImportRun for a corpus, or {\"state\": \"none\"} when nothing has been imported. Counters are\nlive while state == 'running'."},"IngestStatusResp":{"properties":{"queue_depth":{"type":"integer","title":"Queue Depth","default":0},"oldest_backlog_seconds":{"type":"number","title":"Oldest Backlog Seconds","default":0.0},"accepting_uploads":{"type":"boolean","title":"Accepting Uploads","default":true}},"type":"object","title":"IngestStatusResp","description":"The ingestion picture, aggregate only. queue_depth is jobs waiting platform-wide and\noldest_backlog_seconds is the single worst document anywhere — neither identifies a tenant."},"InviteCreateReq":{"properties":{"email":{"type":"string","maxLength":254,"title":"Email"},"role":{"type":"string","pattern":"^(admin|member)$","title":"Role","default":"member"}},"type":"object","required":["email"],"title":"InviteCreateReq","description":"Tenant-admin invites a teammate into their workspace."},"InviteInfoReq":{"properties":{"token":{"type":"string","maxLength":512,"minLength":1,"title":"Token"}},"type":"object","required":["token"],"title":"InviteInfoReq","description":"Look up who an invite is for (the accept page's confirmation header)."},"InviteInfoResp":{"properties":{"email":{"type":"string","title":"Email"},"workspace":{"type":"string","title":"Workspace"}},"type":"object","required":["email","workspace"],"title":"InviteInfoResp"},"IpAllowlistReq":{"properties":{"cidrs":{"items":{"type":"string"},"type":"array","maxItems":200,"title":"Cidrs","default":[]},"confirm_lockout":{"type":"boolean","title":"Confirm Lockout","default":false}},"type":"object","title":"IpAllowlistReq","description":"CIDRs, and the deliberate override. An admin submitting a list their own address falls\noutside is about to lock the workspace out of its own account, so the write refuses unless they\nsay they mean it (they might be configuring from home for an office range)."},"IpAllowlistResp":{"properties":{"cidrs":{"items":{"type":"string"},"type":"array","title":"Cidrs","default":[]},"your_ip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Your Ip"},"your_ip_allowed":{"type":"boolean","title":"Your Ip Allowed","default":true}},"type":"object","title":"IpAllowlistResp","description":"The stored list plus the address we saw this request come from, so the page can show \"your\ncurrent IP\" beside the field instead of sending someone off to look it up."},"JobResp":{"properties":{"id":{"type":"string","title":"Id"},"corpus_id":{"type":"string","title":"Corpus Id"},"kind":{"type":"string","title":"Kind"},"status":{"type":"string","title":"Status"},"detail":{"type":"string","title":"Detail"},"progress":{"type":"number","title":"Progress","default":0.0},"eta_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Eta Seconds"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"parent_job_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Job Id"}},"type":"object","required":["id","corpus_id","kind","status","detail","created_at","updated_at"],"title":"JobResp"},"KmsKeyReq":{"properties":{"key_arn":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Key Arn"}},"type":"object","title":"KmsKeyReq","description":"An empty or absent key_arn clears the key and returns the workspace to the bucket default."},"KmsKeyResp":{"properties":{"key_arn":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key Arn"},"scope":{"type":"string","title":"Scope"},"verified":{"type":"boolean","title":"Verified","default":false}},"type":"object","required":["scope"],"title":"KmsKeyResp","description":"`scope` says in plain words which copies this key covers today — the documents in the\nplatform bucket — so nobody reads more into it than is true (see routers/enterprise.py)."},"LegalAcceptReq":{"properties":{"document":{"type":"string","maxLength":64,"title":"Document"},"version":{"type":"string","maxLength":64,"title":"Version"}},"type":"object","required":["document","version"],"title":"LegalAcceptReq","description":"The version is required, not implied: accepting is a statement about a specific text, so a\npage that has been open since before a version bump cannot accept the new one by accident."},"LegalDocResp":{"properties":{"document":{"type":"string","title":"Document"},"title":{"type":"string","title":"Title"},"version":{"type":"string","title":"Version"},"effective_date":{"type":"string","title":"Effective Date"},"draft":{"type":"boolean","title":"Draft"},"text":{"type":"string","title":"Text"}},"type":"object","required":["document","title","version","effective_date","draft","text"],"title":"LegalDocResp","description":"A legal document as served: version, the date it takes effect, and the text."},"LegalDocStatusResp":{"properties":{"document":{"type":"string","title":"Document"},"title":{"type":"string","title":"Title"},"current_version":{"type":"string","title":"Current Version"},"accepted_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accepted Version"},"accepted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Accepted At"},"accepted_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accepted By"},"current":{"type":"boolean","title":"Current","default":false}},"type":"object","required":["document","title","current_version"],"title":"LegalDocStatusResp","description":"One document's acceptance state for this workspace."},"LegalReceiptResp":{"properties":{"document":{"type":"string","title":"Document"},"title":{"type":"string","title":"Title"},"version":{"type":"string","title":"Version"},"effective_date":{"type":"string","title":"Effective Date"},"draft":{"type":"boolean","title":"Draft"},"text":{"type":"string","title":"Text"},"accepted_at":{"type":"string","format":"date-time","title":"Accepted At"},"accepted_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accepted By"},"accepted_by_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accepted By Email"},"ip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip"},"user_agent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Agent"},"workspace":{"type":"string","title":"Workspace"}},"type":"object","required":["document","title","version","effective_date","draft","text","accepted_at","workspace"],"title":"LegalReceiptResp","description":"The acceptance receipt: the exact text that was accepted, plus who accepted it, when, and\nfrom where. This stands in for a signed PDF — the same facts, machine-readable."},"LegalStatusResp":{"properties":{"documents":{"items":{"$ref":"#/components/schemas/LegalDocStatusResp"},"type":"array","title":"Documents","default":[]},"all_current":{"type":"boolean","title":"All Current","default":false}},"type":"object","title":"LegalStatusResp"},"LinkAccountInfoResp":{"properties":{"email":{"type":"string","title":"Email"},"provider":{"type":"string","title":"Provider"}},"type":"object","required":["email","provider"],"title":"LinkAccountInfoResp","description":"Who the linking screen is about. Reveals nothing the ticket holder could not learn by\ncontinuing — the ticket IS the proof that this sign-in just happened — and the ticket rides the\nPOST body so it stays out of URLs and logs, exactly like invite-info."},"LinkAccountReq":{"properties":{"ticket":{"type":"string","maxLength":2048,"minLength":1,"title":"Ticket"}},"type":"object","required":["ticket"],"title":"LinkAccountReq","description":"PLAT-33 — the ticket from the linking screen, posted when the person presses Continue."},"LinkResp":{"properties":{"status":{"type":"string","title":"Status","default":"ok"},"invite_link":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invite Link"},"reset_link":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reset Link"},"verify_link":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verify Link"}},"type":"object","title":"LinkResp","description":"Generic response for the gated-email flows. When EMAIL_BACKEND=none the link is\nincluded so the flow is completable without an email provider; when a real backend\nis configured the link is emailed and these fields are omitted (None)."},"ManifestFile":{"properties":{"path":{"type":"string","maxLength":1024,"minLength":1,"title":"Path"},"sha256":{"type":"string","maxLength":64,"minLength":64,"pattern":"^[0-9a-fA-F]{64}$","title":"Sha256"},"size":{"type":"integer","minimum":0.0,"title":"Size"},"content_type":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Content Type"}},"type":"object","required":["path","sha256","size"],"title":"ManifestFile","description":"One file as the CLIENT sees it, before anything has been uploaded.\n\n`sha256` is of the RAW BYTES, which is the only hash a client can compute â€” it has the file, not\na parser. The server keeps both that and the hash of the extracted text (they answer different\nquestions: \"do you already have these bytes\" versus \"does this need rebuilding\")."},"ManifestReq":{"properties":{"files":{"items":{"$ref":"#/components/schemas/ManifestFile"},"type":"array","maxItems":10000,"title":"Files"}},"type":"object","title":"ManifestReq","description":"A batch of files. Capped at 10,000 per call so one request can never be an unbounded\ntransaction; a larger sync pages through in chunks, which is what the CLI does."},"McpDocMeta":{"properties":{"id":{"type":"string","title":"Id"},"title":{"type":"string","title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"section_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Section Count"}},"type":"object","required":["id","title"],"title":"McpDocMeta","description":"One document in the MCP introspection listing. `id` is the tenant-namespaced cart id (the same\npinnable currency as /query's used_docs); `title` is the filename-stem display title (matches\nchat's Sources exactly, via retrieval.title_for_filename).\n\nFor a document split into section carts (CAAS-405) `id` is the DOCUMENT id instead, because such a\ndocument has no cart of its own — /query expands it to its sections' carts, so it pins the same\nway. `section_count` is how a client tells the two apart, and is null for everything else."},"McpDocsResp":{"properties":{"total":{"type":"integer","title":"Total"},"documents":{"items":{"$ref":"#/components/schemas/McpDocMeta"},"type":"array","title":"Documents","default":[]}},"type":"object","required":["total"],"title":"McpDocsResp","description":"GET /mcp/{id}/documents — the paged/filtered inventory. `total` is the count AFTER the optional\n`q` filter (so a client can page it), `documents` is the requested page (filename asc)."},"MemberResp":{"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"role":{"type":"string","title":"Role"},"is_active":{"type":"boolean","title":"Is Active","default":true}},"type":"object","required":["id","email","role"],"title":"MemberResp"},"MembersResp":{"properties":{"members":{"items":{"$ref":"#/components/schemas/MemberResp"},"type":"array","title":"Members","default":[]},"invites":{"items":{"$ref":"#/components/schemas/PendingInviteResp"},"type":"array","title":"Invites","default":[]}},"type":"object","title":"MembersResp","description":"GET /admin/members — active members + still-pending invites for the tenant."},"NextInvoice":{"properties":{"total_usd":{"type":"number","title":"Total Usd"},"currency":{"type":"string","title":"Currency","default":"usd"},"due_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Due At"},"lines":{"items":{"$ref":"#/components/schemas/NextInvoiceLine"},"type":"array","title":"Lines","default":[]}},"type":"object","required":["total_usd"],"title":"NextInvoice","description":"Stripe's own preview of the next invoice for this subscription. `total_usd` is Stripe's\namount_due verbatim, never a sum of ours: the page must not show a total the invoice will\ndisagree with."},"NextInvoiceLine":{"properties":{"description":{"type":"string","title":"Description"},"amount_usd":{"type":"number","title":"Amount Usd"},"kind":{"type":"string","title":"Kind","default":"plan"}},"type":"object","required":["description","amount_usd"],"title":"NextInvoiceLine","description":"One row of the next invoice. `kind` is plan | proration | usage | credit | tax; a credit is\na negative amount, so the sign carries that and the copy never has to."},"OnboardingPatchReq":{"properties":{"onboarding_step":{"anyOf":[{"type":"string","pattern":"^(name|documents|model|review|onboarding|ready)$"},{"type":"null"}],"title":"Onboarding Step"},"model_tier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Tier"}},"type":"object","title":"OnboardingPatchReq","description":"Persist the wizard cursor and/or the chosen model tier. Both optional — the frontend PATCHes\nwhichever changed as the user moves through the steps. `model_tier` is validated against the\nserving registry in the router (unknown ids are rejected there)."},"OnboardingStateResp":{"properties":{"corpus_id":{"type":"string","title":"Corpus Id"},"onboarding_step":{"type":"string","title":"Onboarding Step"},"status":{"type":"string","title":"Status"},"model_tier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Tier"},"model_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Ref"},"n_documents":{"type":"integer","title":"N Documents"},"documents":{"items":{"$ref":"#/components/schemas/DocumentResp"},"type":"array","title":"Documents","default":[]}},"type":"object","required":["corpus_id","onboarding_step","status","n_documents"],"title":"OnboardingStateResp","description":"The full resumable-onboarding snapshot: where the wizard is, the chosen tier, and per-document\nparse/onboard status so the frontend can reopen at the exact step and show per-file progress."},"Page_ApiKeyResp_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ApiKeyResp"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["items"],"title":"Page[ApiKeyResp]"},"Page_AuditEventResp_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AuditEventResp"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["items"],"title":"Page[AuditEventResp]"},"Page_CorpusResp_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/CorpusResp"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["items"],"title":"Page[CorpusResp]"},"Page_DocumentResp_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/DocumentResp"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["items"],"title":"Page[DocumentResp]"},"Page_JobResp_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/JobResp"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["items"],"title":"Page[JobResp]"},"Page_SourceResp_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SourceResp"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["items"],"title":"Page[SourceResp]"},"Page_SyncRunResp_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SyncRunResp"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["items"],"title":"Page[SyncRunResp]"},"Page_WebhookResp_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/WebhookResp"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["items"],"title":"Page[WebhookResp]"},"PendingInviteResp":{"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"role":{"type":"string","title":"Role"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","email","role","created_at"],"title":"PendingInviteResp"},"PickerConfigResp":{"properties":{"access_token":{"type":"string","title":"Access Token"},"app_id":{"type":"string","title":"App Id"},"api_key":{"type":"string","title":"Api Key"}},"type":"object","required":["access_token","app_id","api_key"],"title":"PickerConfigResp","description":"GET /connectors/connections/{id}/picker-config (Google Drive only) — the config the browser\nneeds to open the Google Picker. Under drive.file the Picker MUST run with the user's own Drive\naccess token (that's the sanctioned pattern — the token scopes the Picker to this app's grantable\nfiles), so we hand the browser the short-lived access token here. It is never logged and never\npersisted client-side; the Picker session consumes it and the grant it returns is what the import\nthen reads. app_id is the Google Cloud project number (setAppId) and api_key enables the Picker\nJS — without either, picked items don't register as drive.file grants."},"PlanFeaturesResp":{"properties":{"sso":{"type":"boolean","title":"Sso","default":false},"audit_export":{"type":"boolean","title":"Audit Export","default":false},"invoice_billing":{"type":"boolean","title":"Invoice Billing","default":false},"ip_allowlist":{"type":"boolean","title":"Ip Allowlist","default":false},"sync_sources":{"type":"boolean","title":"Sync Sources","default":false},"webhooks":{"type":"boolean","title":"Webhooks","default":false},"kms_key":{"type":"boolean","title":"Kms Key","default":false}},"type":"object","title":"PlanFeaturesResp","description":"The entitlement flags a tier grants. Each is configured by the customer, never by us, and\neach one is enforced by a route — this model mirrors plans.PlanFeatures field for field so the\npricing page renders its badges from data rather than from a list someone has to remember to\nupdate."},"PlanResp":{"properties":{"key":{"type":"string","title":"Key"},"name":{"type":"string","title":"Name"},"monthly_usd":{"type":"number","title":"Monthly Usd"},"annual_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Annual Usd"},"currency":{"type":"string","title":"Currency","default":"usd"},"trial_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Trial Days"},"card_required":{"type":"boolean","title":"Card Required","default":true},"included_documents":{"type":"integer","title":"Included Documents"},"included_queries_per_month":{"type":"integer","title":"Included Queries Per Month"},"seats":{"type":"integer","title":"Seats"},"workspaces":{"type":"integer","title":"Workspaces"},"doc_overage_usd_per_month":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Doc Overage Usd Per Month"},"query_overage_usd_per_1k":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Query Overage Usd Per 1K"},"features":{"$ref":"#/components/schemas/PlanFeaturesResp","default":{"sso":false,"audit_export":false,"invoice_billing":false,"ip_allowlist":false,"sync_sources":false,"webhooks":false,"kms_key":false}},"tagline":{"type":"string","title":"Tagline"},"bullets":{"items":{"type":"string"},"type":"array","title":"Bullets","default":[]}},"type":"object","required":["key","name","monthly_usd","included_documents","included_queries_per_month","seats","workspaces","tagline"],"title":"PlanResp","description":"One self-serve tier as the pricing page renders it. `annual_usd` is null only on the free\ntier; every purchasable tier carries a price, including Enterprise, because no tier has a\n'contact sales' step. `doc_overage_usd_per_month` / `query_overage_usd_per_1k` are null when\nthe tier hard-stops at its allowance instead of metering overage (Free). `card_required` is\nfalse on Free, which is what lets the sign-up page promise no card."},"PlansResp":{"properties":{"plans":{"items":{"$ref":"#/components/schemas/PlanResp"},"type":"array","title":"Plans","default":[]},"doc_overage_usd_per_month":{"type":"number","title":"Doc Overage Usd Per Month"},"query_overage_usd_per_1k":{"type":"number","title":"Query Overage Usd Per 1K"},"document_definition":{"type":"string","title":"Document Definition","default":""},"currency":{"type":"string","title":"Currency","default":"usd"}},"type":"object","required":["doc_overage_usd_per_month","query_overage_usd_per_1k"],"title":"PlansResp","description":"The whole pricing page: tiers cheapest first, the two meter rates overage is billed at so a\ncaller can show 'what happens past the allowance' without a second request, and the sentence\nthat says what a document IS.\n\n`document_definition` matters because the documents column is the one allowance a buyer can\nmisread: they count files, we count carts. Serving the definition beside the number means the\npage can never explain the unit differently from the meter that bills it."},"PlatformInfoResp":{"properties":{"region":{"type":"string","title":"Region"},"region_guidance":{"type":"string","title":"Region Guidance"},"max_source_object_mb":{"type":"integer","title":"Max Source Object Mb"},"supported_extensions":{"items":{"type":"string"},"type":"array","title":"Supported Extensions"}},"type":"object","required":["region","region_guidance","max_source_object_mb","supported_extensions"],"title":"PlatformInfoResp","description":"Public deployment facts a customer needs BEFORE they create a bucket (CAAS-608). Region and\nguidance only — nothing about volumes, tenants, or capacity."},"RegisterReq":{"properties":{"email":{"type":"string","maxLength":254,"title":"Email"},"password":{"type":"string","maxLength":128,"minLength":8,"title":"Password"},"tenant_name":{"type":"string","maxLength":120,"minLength":1,"title":"Tenant Name"}},"type":"object","required":["email","password","tenant_name"],"title":"RegisterReq"},"RequestAccessReq":{"properties":{"email":{"type":"string","maxLength":254,"title":"Email"},"name":{"type":"string","maxLength":120,"minLength":1,"title":"Name"},"tenant_name":{"type":"string","maxLength":120,"minLength":1,"title":"Tenant Name"},"reason":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Reason"}},"type":"object","required":["email","name","tenant_name"],"title":"RequestAccessReq","description":"Public 'request access' (invite-only beta waitlist)."},"ResendVerificationReq":{"properties":{"email":{"type":"string","maxLength":254,"title":"Email"}},"type":"object","required":["email"],"title":"ResendVerificationReq","description":"POST /auth/resend-verification — send the open-signup verification link again. Always\nanswers the same way whether or not the address has an account (no user enumeration); the\nper-email cooldown in signup_guard.py is what stops it being a mail-bomb."},"ResetPasswordReq":{"properties":{"token":{"type":"string","maxLength":512,"minLength":1,"title":"Token"},"password":{"type":"string","maxLength":128,"minLength":8,"title":"Password"}},"type":"object","required":["token","password"],"title":"ResetPasswordReq"},"RoleUpdateReq":{"properties":{"role":{"type":"string","pattern":"^(admin|member)$","title":"Role"}},"type":"object","required":["role"],"title":"RoleUpdateReq"},"ScaleRunResp":{"properties":{"id":{"type":"string","title":"Id"},"corpus_id":{"type":"string","title":"Corpus Id"},"max_concurrency":{"type":"integer","title":"Max Concurrency"},"n_queries":{"type":"integer","title":"N Queries"},"points":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Points"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","corpus_id","max_concurrency","n_queries","points","created_at"],"title":"ScaleRunResp"},"ScaleRunSaveReq":{"properties":{"max_concurrency":{"type":"integer","maximum":64.0,"minimum":1.0,"title":"Max Concurrency"},"n_queries":{"type":"integer","maximum":64.0,"minimum":0.0,"title":"N Queries","default":0},"points":{"items":{"additionalProperties":true,"type":"object"},"type":"array","maxItems":64,"title":"Points"}},"type":"object","required":["max_concurrency"],"title":"ScaleRunSaveReq","description":"A finished scale-test run posted by the Scale Test tab so it can be re-loaded later."},"ScaleTestReq":{"properties":{"queries":{"items":{"type":"string"},"type":"array","maxItems":32,"title":"Queries"},"max_concurrency":{"type":"integer","maximum":24.0,"minimum":1.0,"title":"Max Concurrency","default":24}},"type":"object","title":"ScaleTestReq"},"ScheduledPlanChange":{"properties":{"plan":{"type":"string","title":"Plan"},"plan_name":{"type":"string","title":"Plan Name"},"interval":{"type":"string","title":"Interval","default":"monthly"},"effective_at":{"type":"string","format":"date-time","title":"Effective At"}},"type":"object","required":["plan","plan_name","effective_at"],"title":"ScheduledPlanChange","description":"PLAT-31 — the plan switch Stripe has scheduled for the renewal, from the subscription\nschedule itself, so a downgrade made in the Stripe portal reads the same as one made here."},"ServingUnavailableDetail":{"properties":{"error":{"type":"string","title":"Error","default":"serving_unavailable"},"message":{"type":"string","title":"Message"},"retry_after":{"type":"integer","title":"Retry After"},"state":{"type":"string","title":"State"}},"type":"object","required":["message","retry_after","state"],"title":"ServingUnavailableDetail","description":"CAAS-1303 — the `detail` of the 503 every query path returns when serving cannot answer.\n\nONE shape for every serving-side failure: the box is unreachable, the engine returned a 5xx, the\nengine refused a degraded serve, the box is still booting. An agent should not have to tell\nthose apart, because its response to all four is identical: wait `retry_after` seconds and ask\nagain. So the only thing that varies is `state`, which is there to be shown to a human.\n\nDeclared as a schema rather than assembled inline so the field names cannot drift between the\nnon-streaming 503 body, the stream's terminal frame, and the generated API client.\n\nNOT returned by any ingestion route. Uploads, hashing, diffs, source syncs and parsing are\ncontrol plane: they keep accepting work for the whole outage and the queue drains when serving\ncomes back. That is the promise in slo.py, and `message` says it out loud."},"SessionResp":{"properties":{"access_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Token"},"token_type":{"type":"string","title":"Token Type","default":"bearer"},"renewed":{"type":"boolean","title":"Renewed","default":false},"session":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"renew_after":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Renew After"}},"type":"object","title":"SessionResp","description":"The answer to POST /auth/refresh (PLAT-8).\n\n`access_token` is present ONLY when something was minted (`renewed` true); a token that is still\nearly in its life gets renewed=false and the caller keeps what it has, which is what lets the\nSPA call this on every app load without churning bearers. `expires_at` and `renew_after` always\ndescribe the token the caller holds after the call, so a session's sliding window is observable\nwithout decoding a JWT."},"SharedFolderConnectPickedReq":{"properties":{"oauth_connection_id":{"type":"string","maxLength":64,"title":"Oauth Connection Id"},"folder_id":{"type":"string","maxLength":256,"title":"Folder Id"},"folder_name":{"type":"string","maxLength":512,"title":"Folder Name","default":""}},"type":"object","required":["oauth_connection_id","folder_id"],"title":"SharedFolderConnectPickedReq","description":"POST /connectors/google_drive_shared/connect-picked — a folder the user just picked in the\nGoogle Picker through their OAuth (drive.file) connection. The server shares it with the service\naccount on the user's behalf, so the customer never has to leave the app."},"SharedFolderConnectReq":{"properties":{"folder":{"type":"string","maxLength":2048,"title":"Folder"}},"type":"object","required":["folder"],"title":"SharedFolderConnectReq","description":"POST /connectors/google_drive_shared/connect — the folder the customer shared with that\naddress, as they pasted it: a Drive folder link, or the bare folder id."},"SharedFolderIdentityResp":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"SharedFolderIdentityResp","description":"GET /connectors/google_drive_shared/identity — the address a customer shares a folder with."},"SourceCreateReq":{"properties":{"kind":{"type":"string","enum":["s3","google_drive","sharepoint","google_drive_shared"],"title":"Kind","default":"s3"},"external_id":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"External Id"},"connection_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Connection Id"},"folder_id":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Folder Id"},"site_id":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Site Id"},"bucket":{"type":"string","maxLength":63,"title":"Bucket","default":""},"prefix":{"type":"string","maxLength":1024,"title":"Prefix","default":""},"region":{"type":"string","maxLength":32,"title":"Region","default":""},"role_arn":{"type":"string","maxLength":2048,"title":"Role Arn","default":""},"kms_key_arn":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Kms Key Arn"},"mode":{"type":"string","enum":["additive","mirror"],"title":"Mode","default":"additive"},"schedule_minutes":{"anyOf":[{"type":"integer","maximum":10080.0,"minimum":15.0},{"type":"null"}],"title":"Schedule Minutes"},"allow_cross_region":{"type":"boolean","title":"Allow Cross Region","default":false},"inventory_bucket":{"anyOf":[{"type":"string","maxLength":63},{"type":"null"}],"title":"Inventory Bucket"},"inventory_prefix":{"anyOf":[{"type":"string","maxLength":1024},{"type":"null"}],"title":"Inventory Prefix"}},"type":"object","title":"SourceCreateReq","description":"Register a bucket a document base pulls from.\n\nNote what is NOT here: no access key, no secret, no session token. Registration takes a role ARN\nand nothing else, because the credential model is AssumeRole with an external id we mint — there\nis no long-lived secret for a customer to paste, or for us to store and eventually leak.\n\nCAAS-606 adds the other two kinds. A `google_drive` or `sharepoint` source names a\nConnectorConnection the customer has already linked plus the folder to watch, and carries none of\nthe S3 fields — which is why `bucket` and `role_arn` are optional HERE rather than required: the\nroute rejects a missing one on the `s3` branch, where it actually matters, so a delta\nregistration is not forced to invent a bucket name to satisfy a validator."},"SourceCreateResp":{"properties":{"id":{"type":"string","title":"Id"},"corpus_id":{"type":"string","title":"Corpus Id"},"kind":{"type":"string","title":"Kind"},"bucket":{"type":"string","title":"Bucket"},"prefix":{"type":"string","title":"Prefix"},"region":{"type":"string","title":"Region"},"role_arn":{"type":"string","title":"Role Arn"},"external_id":{"type":"string","title":"External Id"},"kms_key_arn":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kms Key Arn"},"connection_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connection Id"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id"},"site_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Id"},"has_delta_token":{"type":"boolean","title":"Has Delta Token","default":false},"mode":{"type":"string","title":"Mode"},"schedule_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Schedule Minutes"},"status":{"type":"string","title":"Status"},"last_validated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Validated At"},"last_sync_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Sync At"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"inventory_bucket":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inventory Bucket"},"inventory_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inventory Prefix"},"inventory_configured":{"type":"boolean","title":"Inventory Configured","default":false},"last_inventory_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Inventory At"},"inventory_notice":{"type":"string","title":"Inventory Notice","default":""},"document_count":{"type":"integer","title":"Document Count","default":0},"setup":{"anyOf":[{"$ref":"#/components/schemas/SourceSetupResp"},{"type":"null"}]}},"type":"object","required":["id","corpus_id","kind","bucket","prefix","region","role_arn","external_id","mode","status","created_at"],"title":"SourceCreateResp","description":"What registration returns: the source, plus the IAM setup when there is still work to do.\n\n`setup` is populated exactly when the source came back needing validation — the customer\nregistered before applying a trust policy, which is the supported order — so the templates they\nneed are in the same response rather than behind another call. It is null on a source that\nalready validated, because there is nothing left for them to apply."},"SourceRef":{"properties":{"id":{"type":"string","title":"Id"},"title":{"type":"string","title":"Title"}},"type":"object","required":["id","title"],"title":"SourceRef"},"SourceResp":{"properties":{"id":{"type":"string","title":"Id"},"corpus_id":{"type":"string","title":"Corpus Id"},"kind":{"type":"string","title":"Kind"},"bucket":{"type":"string","title":"Bucket"},"prefix":{"type":"string","title":"Prefix"},"region":{"type":"string","title":"Region"},"role_arn":{"type":"string","title":"Role Arn"},"external_id":{"type":"string","title":"External Id"},"kms_key_arn":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kms Key Arn"},"connection_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connection Id"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id"},"site_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Id"},"has_delta_token":{"type":"boolean","title":"Has Delta Token","default":false},"mode":{"type":"string","title":"Mode"},"schedule_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Schedule Minutes"},"status":{"type":"string","title":"Status"},"last_validated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Validated At"},"last_sync_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Sync At"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"inventory_bucket":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inventory Bucket"},"inventory_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inventory Prefix"},"inventory_configured":{"type":"boolean","title":"Inventory Configured","default":false},"last_inventory_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Inventory At"},"inventory_notice":{"type":"string","title":"Inventory Notice","default":""},"document_count":{"type":"integer","title":"Document Count","default":0}},"type":"object","required":["id","corpus_id","kind","bucket","prefix","region","role_arn","external_id","mode","status","created_at"],"title":"SourceResp","description":"A registered source. Carries `external_id` on purpose: it is not a secret FROM the customer,\nit is a secret SHARED WITH them — they have to paste it into their own trust policy, and being\nunable to read it back would make a lost setup unrecoverable."},"SourceSetupResp":{"properties":{"external_id":{"type":"string","title":"External Id"},"bucket":{"type":"string","title":"Bucket"},"prefix":{"type":"string","title":"Prefix"},"platform_account_id":{"type":"string","title":"Platform Account Id"},"platform_region":{"type":"string","title":"Platform Region"},"events_queue_arn":{"type":"string","title":"Events Queue Arn","default":""},"instructions":{"items":{"type":"string"},"type":"array","title":"Instructions","default":[]},"cloudformation":{"type":"string","title":"Cloudformation"},"terraform":{"type":"string","title":"Terraform"}},"type":"object","required":["external_id","bucket","prefix","platform_account_id","platform_region","cloudformation","terraform"],"title":"SourceSetupResp","description":"Everything a customer needs to grant access, in one response: the steps, and the same policy\nrendered both ways so they can use whichever matches how they manage infrastructure."},"SourceUpdateReq":{"properties":{"inventory_bucket":{"anyOf":[{"type":"string","maxLength":63},{"type":"null"}],"title":"Inventory Bucket"},"inventory_prefix":{"anyOf":[{"type":"string","maxLength":1024},{"type":"null"}],"title":"Inventory Prefix"},"mode":{"anyOf":[{"type":"string","enum":["additive","mirror"]},{"type":"null"}],"title":"Mode"},"schedule_minutes":{"anyOf":[{"type":"integer","maximum":10080.0,"minimum":15.0},{"type":"null"}],"title":"Schedule Minutes"},"role_arn":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Role Arn"},"external_id":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"External Id"}},"type":"object","title":"SourceUpdateReq","description":"Change a source that already exists, without deleting and re-registering it.\n\nWHAT IS EDITABLE AND WHAT IS NOT. The bucket and the prefix are the identity of a source and\nwhat the customer's trust policy was written around, so changing them in place would silently\nrepoint a standing grant; that is a new registration. Everything else here is a setting a\ncustomer genuinely revises later:\n\n  * `mode` — additive today, mirror once they trust the bucket to be the truth (or back again).\n  * `schedule_minutes` — how often to look, or null for manual and event-driven only.\n  * `role_arn` / `external_id` — the credential. These matter most: a role ARN typed wrong left\n    a source that could never sync and could never be repaired, only deleted and rebuilt, which\n    meant re-applying the IAM template for a typo.\n  * the inventory destination (CAAS-604), usually from nothing to something once a prefix has\n    grown past the point where listing it every sync is cheap.\n\nEvery field is optional and the route reads `model_fields_set`, so \"not mentioned\" and\n\"cleared\" are different things: omitting `schedule_minutes` leaves the schedule alone, and\nsending it as null turns the schedule off. Sending both inventory fields empty turns inventory\noff and the next sync goes back to listing, which always works."},"SourceValidationResp":{"properties":{"status":{"type":"string","title":"Status"},"message":{"type":"string","title":"Message"},"objects_listed":{"type":"integer","title":"Objects Listed","default":0},"ingestible_sample":{"items":{"type":"string"},"type":"array","title":"Ingestible Sample","default":[]},"skipped_unsupported_type":{"type":"integer","title":"Skipped Unsupported Type","default":0},"skipped_too_large":{"type":"integer","title":"Skipped Too Large","default":0},"skipped_archived":{"type":"integer","title":"Skipped Archived","default":0},"inventory_configured":{"type":"boolean","title":"Inventory Configured","default":false},"last_inventory_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Inventory At"},"inventory_notice":{"type":"string","title":"Inventory Notice","default":""}},"type":"object","required":["status","message"],"title":"SourceValidationResp","description":"The verdict plus what the trial listing actually saw. The skip counts are the answer to \"my\nbucket has 900 files and Engram found 12\" without anyone having to read a log."},"SsoConfigReq":{"properties":{"issuer":{"type":"string","maxLength":512,"minLength":1,"title":"Issuer"},"client_id":{"type":"string","maxLength":255,"minLength":1,"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string","maxLength":1024},{"type":"null"}],"title":"Client Secret"},"email_domains":{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":1,"title":"Email Domains"},"enforced":{"type":"boolean","title":"Enforced","default":false}},"type":"object","required":["issuer","client_id","email_domains"],"title":"SsoConfigReq","description":"What the tenant admin fills in on the security page. `client_secret` is write-only and never\ncomes back out of the API; leaving it empty on an update keeps the stored one, so an admin can\nedit the domain list without re-pasting a secret they may not have kept."},"SsoConfigResp":{"properties":{"enabled":{"type":"boolean","title":"Enabled"},"issuer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Issuer"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret_set":{"type":"boolean","title":"Client Secret Set","default":false},"email_domains":{"items":{"type":"string"},"type":"array","title":"Email Domains","default":[]},"enforced":{"type":"boolean","title":"Enforced","default":false},"redirect_uri":{"type":"string","title":"Redirect Uri"},"start_url":{"type":"string","title":"Start Url"}},"type":"object","required":["enabled","redirect_uri","start_url"],"title":"SsoConfigResp","description":"The stored configuration, secret-free. `client_secret_set` is how the page shows \"a secret is\nsaved\" without ever sending it; `redirect_uri` is the string the admin pastes into their\nprovider, so it comes back even when nothing is configured yet."},"SsoStartReq":{"properties":{"email":{"type":"string","maxLength":254,"title":"Email"}},"type":"object","required":["email"],"title":"SsoStartReq"},"SsoStartResp":{"properties":{"authorization_url":{"type":"string","title":"Authorization Url"},"workspace":{"type":"string","title":"Workspace"}},"type":"object","required":["authorization_url","workspace"],"title":"SsoStartResp","description":"POST /auth/sso/start — where to send the browser. `workspace` lets the sign-in page name the\nworkspace the address resolved to before it redirects."},"SsoTestResp":{"properties":{"authorization_url":{"type":"string","title":"Authorization Url"},"redirect_uri":{"type":"string","title":"Redirect Uri"},"expires_in":{"type":"integer","title":"Expires In"}},"type":"object","required":["authorization_url","redirect_uri","expires_in"],"title":"SsoTestResp","description":"POST /enterprise/sso/test — the authorization URL an admin can open in a new tab to prove the\nround trip works before turning SSO on for their team."},"StatusResp":{"properties":{"state":{"type":"string","title":"State"},"headline":{"type":"string","title":"Headline"},"components":{"items":{"$ref":"#/components/schemas/ComponentResp"},"type":"array","title":"Components","default":[]},"ingest":{"$ref":"#/components/schemas/IngestStatusResp","default":{"queue_depth":0,"oldest_backlog_seconds":0.0,"accepting_uploads":true}},"slo":{"additionalProperties":true,"type":"object","title":"Slo","default":{}},"checked_at":{"type":"number","title":"Checked At","default":0.0}},"type":"object","required":["state","headline"],"title":"StatusResp","description":"What the status page renders and what `engram status` prints."},"SyncRunResp":{"properties":{"id":{"type":"string","title":"Id"},"corpus_id":{"type":"string","title":"Corpus Id"},"source":{"type":"string","title":"Source"},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id"},"state":{"type":"string","title":"State"},"counters":{"additionalProperties":{"type":"integer"},"type":"object","title":"Counters"},"skipped_reasons":{"additionalProperties":{"type":"integer"},"type":"object","title":"Skipped Reasons","default":{}},"documents_total":{"type":"integer","title":"Documents Total","default":0},"documents_done":{"type":"integer","title":"Documents Done","default":0},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"started_at":{"type":"string","format":"date-time","title":"Started At"},"finished_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Finished At"}},"type":"object","required":["id","corpus_id","source","state","counters","started_at"],"title":"SyncRunResp","description":"One ingestion run, whatever fed it (CAAS-109). `counters` is one object rather than a field\neach so a client renders \"3 added, 1 removed\" from a single loop."},"TokenResp":{"properties":{"access_token":{"type":"string","title":"Access Token","default":""},"token_type":{"type":"string","title":"Token Type","default":"bearer"},"checkout_required":{"type":"boolean","title":"Checkout Required","default":false},"verify_link":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verify Link"},"session":{"type":"string","title":"Session","default":"short"},"email_verified":{"type":"boolean","title":"Email Verified","default":true}},"type":"object","title":"TokenResp","description":"A session token, plus what open signup (CAAS-1405/1406) needs the SPA to do next. Both extra\nfields default to the invite-mode answer, so every existing caller's response is byte-identical."},"UploadCredentialsResp":{"properties":{"bucket":{"type":"string","title":"Bucket"},"prefix":{"type":"string","title":"Prefix"},"region":{"type":"string","title":"Region"},"access_key_id":{"type":"string","title":"Access Key Id"},"secret_access_key":{"type":"string","title":"Secret Access Key"},"session_token":{"type":"string","title":"Session Token"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"sync_command":{"type":"string","title":"Sync Command"},"session_policy":{"type":"string","title":"Session Policy"}},"type":"object","required":["bucket","prefix","region","access_key_id","secret_access_key","session_token","expires_at","sync_command","session_policy"],"title":"UploadCredentialsResp","description":"Short-lived credentials scoped to one document base's prefix, so any S3 tool can push into it.\n\n`session_policy` is returned deliberately. A customer handing credentials to a build agent should\nbe able to read exactly what those credentials can do without taking our word for it, and the\npolicy is the honest answer — it is also what makes the scoping testable rather than claimed."},"UploadUrl":{"properties":{"path":{"type":"string","title":"Path"},"method":{"type":"string","title":"Method","default":"PUT"},"url":{"type":"string","title":"Url"},"headers":{"additionalProperties":{"type":"string"},"type":"object","title":"Headers","default":{}},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["path","url","expires_at"],"title":"UploadUrl","description":"Where to PUT one file, and the headers that were SIGNED into that URL. Send different headers\nand S3 rejects the request, so they come back rather than being left to guess at."},"UploadUrlsReq":{"properties":{"files":{"items":{"$ref":"#/components/schemas/ManifestFile"},"type":"array","maxItems":10000,"title":"Files"}},"type":"object","title":"UploadUrlsReq","description":"Files the client wants somewhere to PUT. Same shape as a manifest; `content_type` matters\nhere because it is signed into the URL when present."},"UploadUrlsResp":{"properties":{"uploads":{"items":{"$ref":"#/components/schemas/UploadUrl"},"type":"array","title":"Uploads","default":[]}},"type":"object","title":"UploadUrlsResp"},"UpsertReq":{"properties":{"path":{"type":"string","maxLength":1024,"minLength":1,"title":"Path"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"content_base64":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Base64"},"content_type":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Content Type"},"onboard":{"type":"boolean","title":"Onboard","default":true}},"type":"object","required":["path"],"title":"UpsertReq","description":"Write ONE document by path (CAAS-607) â€” the integrator's endpoint, where the content is\nalready in hand and a three-call presign dance would be silly.\n\nExactly one of `text` or `content_base64`. `text` is the common case by a distance (an agent\nwriting a note, a pipeline emitting markdown) and skips base64 entirely; `content_base64` is\nthere for a PDF or a DOCX. Idempotent on `path`: the same path twice updates in place."},"UsagePointResp":{"properties":{"date":{"type":"string","title":"Date"},"queries":{"type":"integer","title":"Queries","default":0}},"type":"object","required":["date"],"title":"UsagePointResp","description":"One day of the ~30-day served-query series."},"UsageResp":{"properties":{"queries":{"type":"integer","title":"Queries","default":0},"documents":{"type":"integer","title":"Documents","default":0},"storage_gb":{"type":"number","title":"Storage Gb","default":0.0},"gpu_seconds":{"type":"number","title":"Gpu Seconds","default":0.0},"n_corpora":{"type":"integer","title":"N Corpora","default":0},"by_corpus":{"items":{"$ref":"#/components/schemas/CorpusUsageResp"},"type":"array","title":"By Corpus","default":[]},"series":{"items":{"$ref":"#/components/schemas/UsagePointResp"},"type":"array","title":"Series","default":[]}},"type":"object","title":"UsageResp","description":"GET /admin/usage — tenant-scoped usage rollup + a daily query series."},"UsedDocument":{"properties":{"id":{"type":"string","title":"Id"},"filename":{"type":"string","title":"Filename"},"title":{"type":"string","title":"Title"},"sections":{"items":{"$ref":"#/components/schemas/UsedSection"},"type":"array","title":"Sections","default":[]}},"type":"object","required":["id","filename","title"],"title":"UsedDocument","description":"One FILE an answer drew on (CAAS-403). `id` is the document id — pinnable as a whole file,\nwhich expands to its sections' carts on the next turn. `sections` is empty for a document that\nwas never split, so a client renders one list rather than two code paths."},"UsedSection":{"properties":{"cart_id":{"type":"string","title":"Cart Id"},"section_index":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Section Index"},"section_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Section Title"}},"type":"object","required":["cart_id"],"title":"UsedSection","description":"One section of a document an answer drew on (CAAS-403). `cart_id` is the pinnable id that\nappeared in used_docs, so a client can pin this section alone if it wants to narrow."},"UserResp":{"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"tenant_id":{"type":"string","title":"Tenant Id"},"tenant_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Name"},"role":{"type":"string","title":"Role"},"platform_admin":{"type":"boolean","title":"Platform Admin","default":false}},"type":"object","required":["id","email","tenant_id","role"],"title":"UserResp"},"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"},"WebhookCreateReq":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":8,"title":"Url"},"events":{"items":{"type":"string"},"type":"array","title":"Events","default":[]}},"type":"object","required":["url"],"title":"WebhookCreateReq","description":"Subscribe a URL to events. `events` empty means every event, which is what someone who just\nwants to be told things gets without enumerating a list they would then have to maintain."},"WebhookResp":{"properties":{"id":{"type":"string","title":"Id"},"url":{"type":"string","title":"Url"},"secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secret"},"events":{"items":{"type":"string"},"type":"array","title":"Events"},"active":{"type":"boolean","title":"Active"},"last_delivery_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Delivery At"},"last_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Status"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["id","url","events","active","created_at"],"title":"WebhookResp","description":"The endpoint, and for a workspace admin its signing secret.\n\n`secret` IS readable back (an admin who lost the HMAC key cannot verify a delivery until they\ncan read it again), but ONLY BY A WORKSPACE ADMIN — a session whose role is admin, or an API key\ncarrying the `admin` scope. For every other caller it is null. Subscribing and unsubscribing are\nadmin-only, and anything holding the key can forge a delivery the customer's receiver will accept\nas ours, so the listing must not be the one place that hands it to every member."},"WebhookUpdateReq":{"properties":{"active":{"type":"boolean","title":"Active"}},"type":"object","required":["active"],"title":"WebhookUpdateReq","description":"Pause or resume an endpoint. One field, because pausing is the only thing worth changing in\nplace: a different URL is a different receiver and deserves its own secret, and a different\nevent list is one call to delete and one to subscribe.\n\nPaused means SILENT, not buffered. Nothing is queued while an endpoint is off, so resuming\nstarts the next event and never replays the ones missed — an endpoint that comes back to a\nbacklog of stale \"your run finished\" calls is worse than one that comes back quiet."},"WorkspaceSetupReq":{"properties":{"tenant_name":{"type":"string","maxLength":120,"minLength":1,"title":"Tenant Name"},"name":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Name"}},"type":"object","required":["tenant_name"],"title":"WorkspaceSetupReq","description":"PLAT-32 — the one step between a Google sign-up and the app: name the workspace, and confirm\nthe display name Google gave us. Same two fields, same names and the same rules as the email\nsignup path (RegisterReq.tenant_name, AcceptInviteReq.name), because it is the same question\nasked in a different order."}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"/auth/login"}}}}},"tags":[{"name":"contract","description":"Version discovery for the published API."},{"name":"auth","description":"Sign-in, registration, invites, and SSO."},{"name":"api-keys","description":"Tenant API keys: create, list, revoke. Scoped and hashed at rest."},{"name":"corpora","description":"Document bases: the unit a customer owns, queries and shares."},{"name":"ingest","description":"The ingestion contract — diff, upload-urls, commit, upsert, sync runs."},{"name":"jobs","description":"Onboarding work: train, describe, cancel, progress."},{"name":"chat","description":"Retrieval-backed answers, and the MCP REST surface an agent calls."},{"name":"sources","description":"Pull sources (S3 and connectors) and vended upload credentials."},{"name":"webhooks","description":"HMAC-signed delivery of ingestion events."},{"name":"audit","description":"Audit trail and deletion receipts."},{"name":"enterprise","description":"SSO, IP allowlist, customer-managed keys, usage export."},{"name":"legal","description":"Terms and DPA: the documents, acceptance, and receipts."},{"name":"billing","description":"Subscription state, checkout, plan changes."},{"name":"plans","description":"Public pricing and plan entitlements."},{"name":"status","description":"Live serving state and the published SLOs."}]}