{"openapi":"3.1.0","info":{"title":"Platform API","version":"1.0.0","summary":"Public and session-authenticated HTTP API for the content platform.","description":"HTTP API surface for a hosted healthcare AI multi-agent content platform. Public discovery endpoints are unauthenticated. State-changing endpoints require a valid session cookie and enforce same-origin (CSRF) protection, except the payment-provider webhook, which is authenticated by its signature and is not callable by customers. This document is generated from the live route handlers and their validators; it describes only endpoints that exist in this deployment.","contact":{"name":"Platform Operations"}},"servers":[{"url":"/","description":"Current deployment (relative to the serving origin)."}],"tags":[{"name":"Operations","description":"Public infrastructure endpoints (health/liveness probes)."},{"name":"Discovery","description":"Public, unauthenticated metadata endpoints for crawlers and integrations."},{"name":"Ingestion","description":"Session-authenticated document ingestion."},{"name":"Compliance","description":"Session-authenticated, admin-gated organization data export."},{"name":"Chat","description":"Session-authenticated chat answered by the multi-agent system, streamed as NDJSON."},{"name":"Design","description":"Session-authenticated asynchronous video generation (submit, poll, then stream the finished video)."},{"name":"Billing","description":"Payment-provider webhooks. Server-to-server and signature-authenticated; not callable by customers."}],"paths":{"/api/health":{"get":{"operationId":"getHealth","summary":"Liveness and readiness probe","description":"Returns process uptime, server time, presence-only environment readiness booleans (never any secret values) and a bounded database reachability check. 200 when ready, 503 when not configured or the database did not answer. Not cached.","tags":["Operations"],"responses":{"200":{"description":"Ready: configured and the database answered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"503":{"description":"Degraded: required env missing or the database did not answer in time.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/llms.txt":{"get":{"operationId":"getLlmsTxt","summary":"Machine-readable product description","description":"Returns the llms.txt product overview (capabilities, data handling, plans, discovery links) as plain text for AI crawlers. Cacheable.","tags":["Discovery"],"responses":{"200":{"description":"Plain-text product description.","content":{"text/plain":{"schema":{"type":"string","description":"llms.txt formatted content."}}}}}}},"/agents.json":{"get":{"operationId":"getAgentsCatalog","summary":"Agent preset catalog","description":"Returns the agent preset catalog (preset and agent shape: id, name, expertise, tier, dependencies). Discovery metadata only; internal prompt text is not disclosed. Cacheable.","tags":["Discovery"],"responses":{"200":{"description":"Agent preset catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentCatalog"}}}}}}},"/api/openapi.json":{"get":{"operationId":"getOpenApiDocument","summary":"This OpenAPI document","description":"Returns this OpenAPI 3.1 document describing the platform API. Cacheable.","tags":["Discovery"],"responses":{"200":{"description":"OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object","description":"OpenAPI 3.1 document."}}}}}}},"/api/ingest":{"post":{"operationId":"ingestDocument","summary":"Ingest a reference document","description":"Accepts a reference document for the authenticated caller in one of two modes: a multipart file upload (DOCX, TXT, or MD; parsed in-process) or a JSON body containing an already client-parsed document (PDF text). Each successful ingest is written to the append-only audit log. Returns the canonical parsed document.","tags":["Ingestion"],"security":[{"sessionCookie":[]}],"requestBody":{"description":"Either application/json with a client-parsed document, or multipart/form-data with a 'file' field.","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestJsonRequest"}},"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"DOCX, TXT, or MD file (max 10 MB). PDF must be posted as JSON instead."}}}}}},"responses":{"200":{"description":"Document ingested. Returns the canonical parsed document.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParsedDocument"}}}},"400":{"description":"Invalid JSON body, missing file field, or unexpected content type.","x-problem-codes":["invalid_json","unsupported_request_format","missing_file"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"No valid session.","x-problem-codes":["unauthenticated"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Request failed the same-origin check.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginForbidden"}}}},"413":{"description":"Uploaded file exceeds the 10 MB limit.","x-problem-codes":["payload_too_large"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"415":{"description":"Unsupported format, or a PDF was uploaded via multipart (post it as JSON instead).","x-problem-codes":["unsupported_format","pdf_not_parsed_here","file_type_mismatch"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Parsed-document body failed schema validation (invalid_request, with issues), or the file could not be parsed (unparseable_document, with a reference).","x-problem-codes":["invalid_request","unparseable_document"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded (rate_limited, per-user or per-tenant). Includes a Retry-After header.","headers":{"Retry-After":{"description":"Seconds until a retry can succeed (RFC 9110 section 10.2.3). Always sent with rate_limited.","required":true,"schema":{"type":"integer","minimum":1}}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/TooManyProblem"}}}},"500":{"description":"Internal parse contract violation.","x-problem-codes":["internal_error"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/api/compliance/export":{"post":{"operationId":"exportOrganizationData","summary":"Export organization data (right of access)","description":"Returns a JSON snapshot of the caller's organization (members, plan/usage summary, productions, audit history) as a downloadable attachment. Admin/owner only; tenant-scoped; rate-limited; the export itself is recorded in the audit log. No PHI in the default configuration. POST (not GET) because it writes one audit row.","tags":["Compliance"],"security":[{"sessionCookie":[]}],"responses":{"200":{"description":"A JSON data export (Content-Disposition: attachment).","content":{"application/json":{"schema":{"type":"object","description":"Organization data snapshot."}}}},"401":{"description":"No valid session.","x-problem-codes":["unauthenticated"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"No active membership, insufficient role (admin/owner required), or failed same-origin check.","x-problem-codes":["no_active_membership","admin_role_required"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}},"application/json":{"schema":{"$ref":"#/components/schemas/OriginForbidden"}}}},"429":{"description":"Rate limit exceeded (rate_limited). Includes a Retry-After header.","headers":{"Retry-After":{"description":"Seconds until a retry can succeed (RFC 9110 section 10.2.3). Always sent with rate_limited.","required":true,"schema":{"type":"integer","minimum":1}}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/TooManyProblem"}}}},"500":{"description":"Export could not be generated (opaque reference returned).","x-problem-codes":["internal_error"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/api/chat":{"post":{"operationId":"streamChat","summary":"Ask the multi-agent system (streamed)","description":"Answers the conversation with the chosen model and streams the answer as newline-delimited JSON events (see ChatStreamEvent). Session-authenticated, same-origin (CSRF) enforced, rate-limited per user and per tenant, one request in flight per user, and admitted by the spend gate (plan, Free daily cap, monthly allowance) before any model call; an unreadable plan or meter refuses with 503 rather than guessing. Checks run in this order: session (401), same-origin (403), per-user rate limit (429), JSON (400), schema (422), item risk (403), protected health information (422), model (400), active membership (403), per-tenant rate limit (429), in flight (429), spend gate (403/429/402/503), then the team and compare gates. Once the stream opens the status is 200; a later failure arrives as an error event. Aborting the request aborts the upstream model call; aborted calls are still metered.","tags":["Chat"],"security":[{"sessionCookie":[]}],"requestBody":{"description":"The conversation, the model, and at most one answer mode (multiAgent, team or compare).","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatRequest"}}}},"responses":{"200":{"description":"The answer stream: application/x-ndjson; charset=utf-8, one ChatStreamEvent per line, never cached.","headers":{"X-Request-Id":{"description":"Request id. Echoes an acceptable inbound X-Request-Id (at most 200 characters) or a fresh one; quote it when reporting a problem.","schema":{"type":"string","maxLength":200}},"Cache-Control":{"description":"Always no-store.","schema":{"type":"string","enum":["no-store"]}},"X-Accel-Buffering":{"description":"Always no: asks intermediate proxies not to buffer the stream.","schema":{"type":"string","enum":["no"]}}},"content":{"application/x-ndjson":{"schema":{"$ref":"#/components/schemas/ChatStreamEvent"}}}},"400":{"description":"Invalid JSON body; unknown or unavailable model (also for a team agent or a compared model); team and multiAgent together; compare with team or multiAgent; unknown team.","x-problem-codes":["invalid_json","unknown_model","conflicting_modes","unknown_team","unknown_agent_model","too_many_models","no_models_selected"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"No valid session.","x-problem-codes":["unauthenticated"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"This month's included usage is used up and pay-as-you-go is not enabled (monthly allowance gate).","x-problem-codes":["included_usage_used_up"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Failed same-origin check; the itemRisk item was refused by the server-side risk gate; no active tenant membership; the model (or a team agent's or a compared model) is not in the caller's plan; no model in the caller's plan can run the team.","x-problem-codes":["request_refused","no_active_membership","model_not_in_plan","no_model_in_plan"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}},"application/json":{"schema":{"$ref":"#/components/schemas/OriginForbidden"}}}},"422":{"description":"Body failed schema validation (issues lists the Zod messages); or a user turn contains a Social Security number, medical record number or date of birth (phi_detected: the detail names the kinds, never the values; nothing is sent to a model or metered).","x-problem-codes":["invalid_request","phi_detected"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"RFC 9457 problem. code rate_limited (per-user or per-tenant burst limit; Retry-After is the wait), request_in_flight (the caller's previous message is still running; no Retry-After), daily_limit_reached (Free daily cap; Retry-After is the time to 00:00 UTC) or compare_exceeds_daily_limit (a compare needs more of today's messages than are left; Retry-After is the time to 00:00 UTC).","headers":{"Retry-After":{"description":"Seconds until a retry can succeed (RFC 9110 section 10.2.3). Sent with rate_limited, daily_limit_reached and compare_exceeds_daily_limit; ABSENT with request_in_flight, whose wait is unknown.","required":false,"schema":{"type":"integer","minimum":1}}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/TooManyProblem"}}}},"503":{"description":"The plan, the daily count or the monthly spend could not be read, so nothing was run.","x-problem-codes":["usage_unavailable"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/api/design/video":{"post":{"operationId":"submitVideoJob","summary":"Submit a video-generation job","description":"Submits an asynchronous video render and returns a handle to poll with GET. Session-authenticated, same-origin (CSRF) enforced, active tenant membership required, rate-limited per user and per tenant, one submit in flight per user, and subject to the Free daily video cap (fails closed: an unreadable count is a 503). The prompt is screened by the safety pre-filter before it can reach a provider. One usage row is recorded per accepted submit. Checks run in this order: session (401), same-origin (403), per-user rate limit (429), membership (403), per-tenant rate limit (429), body size (413), JSON (400), schema (422), safety filter (422), in flight (429), daily cap (429/503), provider. A body that fails the size check, JSON, schema or the safety filter is refused before the in-flight check and the daily-cap read.","tags":["Design"],"security":[{"sessionCookie":[]}],"requestBody":{"description":"The prompt and optional render settings, at most 13744 bytes.","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoSubmitRequest"}}}},"responses":{"202":{"description":"Job accepted.","headers":{"X-Request-Id":{"description":"Request id. Echoes an acceptable inbound X-Request-Id (at most 200 characters) or a fresh one; quote it when reporting a problem.","schema":{"type":"string","maxLength":200}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoJobHandle"}}}},"400":{"description":"The body is not valid UTF-8 JSON, or names a model the provider does not declare.","x-problem-codes":["invalid_json","video_request_rejected"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"No valid session.","x-problem-codes":["unauthenticated"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Failed same-origin check, or no active tenant membership.","x-problem-codes":["no_active_membership"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}},"application/json":{"schema":{"$ref":"#/components/schemas/OriginForbidden"}}}},"404":{"description":"Video generation is not available: it is out of launch and its server-side switch is off. Nothing is consumed.","x-problem-codes":["video_disabled"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"413":{"description":"The body (by its declared Content-Length, or by the bytes actually read) exceeds 13744 bytes, the largest body the schema can accept.","x-problem-codes":["payload_too_large"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Body failed schema validation (issues lists the Zod messages), or the prompt was refused by the safety filter (error carries the user-facing reason).","x-problem-codes":["invalid_request","prompt_not_allowed"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"RFC 9457 problem. code rate_limited (per-user or per-tenant; Retry-After is the wait), request_in_flight (the caller's previous submit is still running; no Retry-After) or daily_limit_reached (Free daily video cap; Retry-After is the time to 00:00 UTC).","headers":{"Retry-After":{"description":"Seconds until a retry can succeed (RFC 9110 section 10.2.3). Sent with rate_limited, daily_limit_reached and compare_exceeds_daily_limit; ABSENT with request_in_flight, whose wait is unknown.","required":false,"schema":{"type":"integer","minimum":1}}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/TooManyProblem"}}}},"500":{"description":"Unexpected failure starting the job (reference returned).","x-problem-codes":["internal_error"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"The video provider failed or returned an unusable response.","x-problem-codes":["video_provider_unavailable"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Video generation is not configured, or the daily usage count could not be read.","x-problem-codes":["video_not_configured","usage_unavailable"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"operationId":"getVideoJobStatus","summary":"Poll a video-generation job","description":"Returns the current state of a job the caller submitted. Session-authenticated; read-only, so no same-origin check and no rate limit (a poller fires every few seconds by design). A job id the caller did not submit, or one older than the 24-hour ownership window, is answered 404 so its existence is never confirmed. The poll goes to the provider the job was submitted to. A failed job reports a fixed, general message; a completed job's createdAt is when it was submitted, and its video.url is the platform's content address for the job (GET /api/design/video/content). Job ownership is not shared across server instances: poll soon after submitting, and treat a 404 for a job you did submit as the job being no longer pollable.","tags":["Design"],"security":[{"sessionCookie":[]}],"parameters":[{"name":"jobId","in":"query","required":true,"description":"The jobId from the submit response.","schema":{"type":"string","minLength":1,"maxLength":256}},{"name":"provider","in":"query","required":false,"description":"Optional: the provider from the submit response. Omitted, the job's own provider is used. A provider that is not registered is answered 400; one other than the job's own provider is answered 404, as for an unknown job.","schema":{"type":"string"}}],"responses":{"200":{"description":"Current job state.","headers":{"X-Request-Id":{"description":"Request id. Echoes an acceptable inbound X-Request-Id (at most 200 characters) or a fresh one; quote it when reporting a problem.","schema":{"type":"string","maxLength":200}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoJobState"}}}},"400":{"description":"jobId is missing, empty or longer than 256 characters, or provider is not a registered provider.","x-problem-codes":["missing_job_id","unknown_provider","video_request_rejected"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"No valid session.","x-problem-codes":["unauthenticated"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No job with this id was submitted by the caller (or its ownership record expired), or provider is not the provider the job was submitted to.","x-problem-codes":["job_not_found"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"A provider moderation refusal while polling. The handler maps it, but the current provider adapter never raises one on a poll.","x-problem-codes":["prompt_not_allowed"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected failure checking the job (reference returned).","x-problem-codes":["internal_error"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"The video provider failed or returned an unusable response.","x-problem-codes":["video_provider_unavailable"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Video generation is not configured.","x-problem-codes":["video_not_configured"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/api/design/video/content":{"get":{"operationId":"getVideoJobContent","summary":"Stream a completed video","description":"Streams the video of a completed job the caller submitted; a completed poll returns this address as result.video.url. Session-authenticated; read-only, so no same-origin check and no rate limit (a media element makes several requests per play). The platform reads the video from the provider server-side, so no provider address or credential reaches the caller. Authorized exactly as the poll is: a job the caller did not submit, one older than the 24-hour ownership window, or one asked for under another provider is answered 404. A job that no poll has yet reported completed is answered 409. The body is streamed, not buffered, with the provider's video media type (parameters dropped); any other media type is refused 502. One byte range (bytes=first-last, bytes=first- or bytes=-suffix) is passed on to the provider, and any other Range value is ignored, so the whole video is sent. The answer is 206 only when the provider itself answers the range; a provider that does not support ranges sends the whole video (200), and seeking is then limited to what the browser has already loaded. Job ownership is not shared across server instances, as for the poll.","tags":["Design"],"security":[{"sessionCookie":[]}],"parameters":[{"name":"jobId","in":"query","required":true,"description":"The jobId from the submit response.","schema":{"type":"string","minLength":1,"maxLength":256}},{"name":"provider","in":"query","required":false,"description":"Optional: the provider from the submit response. Omitted, the job's own provider is used. A provider that is not registered is answered 400; one other than the job's own provider is answered 404.","schema":{"type":"string"}},{"name":"Range","in":"header","required":false,"description":"Optional: one byte range. Several ranges, another unit or a reversed range are ignored.","schema":{"type":"string"}}],"responses":{"200":{"description":"The whole video.","headers":{"Content-Length":{"description":"Length of this body in bytes, when the provider states it.","required":false,"schema":{"type":"integer","minimum":0}},"Cache-Control":{"description":"The bytes are private to the caller and never stored by a cache.","required":true,"schema":{"type":"string","enum":["private, no-store"]}},"X-Content-Type-Options":{"description":"The media type is not to be sniffed.","required":true,"schema":{"type":"string","enum":["nosniff"]}},"Content-Disposition":{"description":"inline, with a filename made of video- and the job id's letters, digits, hyphens and underscores, plus an extension for mp4, webm, quicktime and ogg video.","required":true,"schema":{"type":"string"}},"Cross-Origin-Resource-Policy":{"description":"Another site cannot embed the bytes.","required":true,"schema":{"type":"string","enum":["same-origin"]}},"Accept-Ranges":{"description":"bytes, sent only when the provider itself advertises byte ranges.","required":false,"schema":{"type":"string","enum":["bytes"]}},"X-Request-Id":{"description":"Request id. Echoes an acceptable inbound X-Request-Id (at most 200 characters) or a fresh one; quote it when reporting a problem.","schema":{"type":"string","maxLength":200}}},"content":{"video/*":{"schema":{"type":"string","format":"binary","description":"The video bytes, streamed as the provider sends them. The media type is the provider's video type."}}}},"206":{"description":"The requested byte range, as the provider answered it.","headers":{"Content-Length":{"description":"Length of this body in bytes, when the provider states it.","required":false,"schema":{"type":"integer","minimum":0}},"Cache-Control":{"description":"The bytes are private to the caller and never stored by a cache.","required":true,"schema":{"type":"string","enum":["private, no-store"]}},"X-Content-Type-Options":{"description":"The media type is not to be sniffed.","required":true,"schema":{"type":"string","enum":["nosniff"]}},"Content-Disposition":{"description":"inline, with a filename made of video- and the job id's letters, digits, hyphens and underscores, plus an extension for mp4, webm, quicktime and ogg video.","required":true,"schema":{"type":"string"}},"Cross-Origin-Resource-Policy":{"description":"Another site cannot embed the bytes.","required":true,"schema":{"type":"string","enum":["same-origin"]}},"Accept-Ranges":{"description":"bytes, sent only when the provider itself advertises byte ranges.","required":false,"schema":{"type":"string","enum":["bytes"]}},"X-Request-Id":{"description":"Request id. Echoes an acceptable inbound X-Request-Id (at most 200 characters) or a fresh one; quote it when reporting a problem.","schema":{"type":"string","maxLength":200}},"Content-Range":{"description":"The range in this body, as the provider stated it (bytes first-last/length).","required":true,"schema":{"type":"string"}}},"content":{"video/*":{"schema":{"type":"string","format":"binary","description":"The video bytes, streamed as the provider sends them. The media type is the provider's video type."}}}},"400":{"description":"jobId is missing, empty or longer than 256 characters, or provider is not a registered provider.","x-problem-codes":["missing_job_id","unknown_provider"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"No valid session.","x-problem-codes":["unauthenticated"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No job with this id was submitted by the caller (or its ownership record expired), or provider is not the provider the job was submitted to. One body for all three, so a job's existence is never confirmed.","x-problem-codes":["job_not_found"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"The job exists but no poll has reported it completed yet.","x-problem-codes":["video_not_ready"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"416":{"description":"The provider cannot satisfy the requested range.","x-problem-codes":["range_not_satisfiable"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"headers":{"Content-Range":{"description":"bytes */length, when the provider states the length.","required":false,"schema":{"type":"string"}}}},"500":{"description":"Unexpected failure loading the video (reference returned).","x-problem-codes":["internal_error"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"The provider failed, did not answer in time, redirected, answered something other than a video media type, or sent malformed range or encoding headers. A fixed message is returned; the provider's own text is not.","x-problem-codes":["video_provider_unavailable"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Video generation is not configured.","x-problem-codes":["video_not_configured"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/api/billing/webhook":{"post":{"operationId":"receiveStripeWebhook","summary":"Stripe webhook receiver (not callable by customers)","description":"Server-to-server endpoint called by Stripe; not callable by customers and not part of the customer API. There is no session and no same-origin check: the Stripe-Signature header (HMAC-SHA256 over the raw body with the platform webhook secret, 300-second replay window) is the authentication, and it is checked before the body is parsed or logged. DORMANT: a verified event is logged (metadata only) and acknowledged; no plan or entitlement change is made. Every event type is acknowledged the same way - there is no unknown-event refusal.","tags":["Billing"],"security":[{"stripeSignature":[]}],"parameters":[{"name":"Stripe-Signature","in":"header","required":true,"description":"Stripe's t=<timestamp>,v1=<signature> header. Missing or invalid: 400.","schema":{"type":"string"}}],"requestBody":{"description":"The raw Stripe event (UTF-8), at most 1 MiB. Verified byte-for-byte before it is parsed.","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeWebhookEvent"}}}},"responses":{"200":{"description":"Verified and acknowledged (any event type).","headers":{"X-Request-Id":{"description":"Request id. Echoes an acceptable inbound X-Request-Id (at most 200 characters) or a fresh one; quote it when reporting a problem.","schema":{"type":"string","maxLength":200}}},"content":{"application/json":{"schema":{"type":"object","required":["received"],"properties":{"received":{"type":"boolean","const":true}}}}}},"400":{"description":"The body could not be read or is not valid UTF-8; the signature is missing, invalid or outside the replay window (signature verification failed); or a verified body is not a well-formed Stripe event (malformed event payload, reference returned).","x-problem-codes":["unreadable_body","signature_invalid","malformed_event"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"413":{"description":"The body (by its declared Content-Length, or by the bytes actually read) exceeds 1 MiB.","x-problem-codes":["payload_too_large"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Webhook verification is not configured on the server (reference returned).","x-problem-codes":["webhook_not_configured"],"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}},"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"session","description":"Session established via the application sign-in flow. State-changing requests must also be same-origin (the server enforces a CSRF origin check)."},"stripeSignature":{"type":"apiKey","in":"header","name":"Stripe-Signature","description":"Stripe webhook signature over the raw request body. Held by Stripe only; customers cannot call the operations that require it."}},"schemas":{"Problem":{"type":"object","description":"RFC 9457 problem details for every API error except 429 (see TooManyProblem) and the same-origin guard's 403 (see OriginForbidden). Switch on `code`; `error` repeats `detail` for clients that predate the problem format.","required":["type","title","status","detail","code","error"],"properties":{"type":{"type":"string","description":"Problem type URI, urn:robotdesktop:problem:<code with hyphens>."},"title":{"type":"string","description":"Short summary of the problem type."},"status":{"type":"integer","minimum":400,"maximum":599},"detail":{"type":"string","description":"Human-readable explanation for this occurrence."},"code":{"type":"string","description":"Stable reason code.","enum":["invalid_json","unreadable_body","unsupported_request_format","missing_file","unknown_model","unknown_agent_model","unknown_team","unknown_provider","conflicting_modes","too_many_models","no_models_selected","missing_job_id","video_request_rejected","signature_invalid","malformed_event","unauthenticated","included_usage_used_up","no_active_membership","admin_role_required","request_refused","model_not_in_plan","no_model_in_plan","job_not_found","video_disabled","video_not_ready","payload_too_large","unsupported_format","file_type_mismatch","pdf_not_parsed_here","range_not_satisfiable","invalid_request","unparseable_document","prompt_not_allowed","phi_detected","internal_error","video_provider_unavailable","usage_unavailable","video_not_configured","webhook_not_configured"]},"error":{"type":"string","description":"Same text as detail (compatibility with the error envelope)."},"issues":{"type":"array","description":"Validation messages (Zod); present on invalid_request.","items":{"type":"string"}},"reference":{"type":"string","description":"Opaque support reference the caller can quote to support. Present only on responses whose handler generates one; it never carries exception text."}}},"OriginForbidden":{"type":"object","description":"The same-origin (CSRF) guard's refusal, sent before the handler runs. It predates the problem format and keeps the bare envelope.","required":["error"],"properties":{"error":{"type":"string","description":"forbidden: <reason>."}}},"TooManyProblem":{"type":"object","description":"RFC 9457 problem details for a 429. Retry-After is sent whenever the wait is known: with rate_limited, daily_limit_reached and compare_exceeds_daily_limit, never with request_in_flight.","required":["type","title","status","detail","code","error"],"properties":{"type":{"type":"string","description":"Problem type URI, urn:robotdesktop:problem:<code with hyphens>."},"title":{"type":"string","description":"Short summary of the problem type."},"status":{"type":"integer","minimum":429,"maximum":429},"detail":{"type":"string","description":"Human-readable explanation for this occurrence."},"code":{"type":"string","description":"Stable reason code.","enum":["rate_limited","request_in_flight","daily_limit_reached","compare_exceeds_daily_limit"]},"error":{"type":"string","description":"Same text as detail (compatibility with the error envelope)."}}},"HealthResponse":{"type":"object","description":"Liveness/readiness snapshot. Env checks are presence booleans only - never values.","required":["status","uptimeSeconds","timestamp","checks"],"properties":{"status":{"type":"string","enum":["ok","degraded"],"description":"ok when configured and the database answered; degraded otherwise (HTTP 503)."},"uptimeSeconds":{"type":"number","minimum":0,"description":"Process uptime in seconds."},"timestamp":{"type":"string","format":"date-time","description":"Server time at response."},"checks":{"type":"object","description":"Readiness sub-checks.","properties":{"server":{"type":"string","description":"Server check result."},"configured":{"type":"boolean","description":"Aggregate readiness flag: all required server env is present (no values, no per-secret names)."},"dbReachable":{"type":"boolean","description":"The database answered `select 1` within 3 seconds (direct Postgres, not the Supabase data API)."}}}}},"ParsedDocument":{"type":"object","description":"Canonical parsed-document contract. No PHI in the default configuration.","required":["text","markdown","pageCount","wordCount","format","truncated","backend","warnings"],"properties":{"text":{"type":"string","description":"Extracted plain text (canonical)."},"markdown":{"type":"string","description":"Light markdown rendering of the document."},"pageCount":{"type":"integer","minimum":0,"description":"Pages (PDF) or 1 for flat formats."},"wordCount":{"type":"integer","minimum":0,"description":"Word count of the extracted text."},"format":{"type":"string","enum":["pdf","docx","txt","md","unknown"],"description":"Detected source format."},"truncated":{"type":"boolean","description":"True if text was capped at the maximum length."},"backend":{"type":"string","description":"Identifier of the parser backend that produced this."},"warnings":{"type":"array","description":"Non-fatal parser notes.","items":{"type":"string"}}}},"IngestJsonRequest":{"type":"object","description":"JSON intake of a client-parsed document (used for PDFs parsed in the browser).","required":["parsed"],"properties":{"parsed":{"$ref":"#/components/schemas/ParsedDocument"},"filename":{"type":"string","description":"Optional original filename for the audit record."}}},"AgentCatalog":{"type":"object","description":"Machine-readable agent preset catalog (discovery metadata only).","required":["version","description","presets"],"properties":{"version":{"type":"string","description":"Catalog schema version."},"description":{"type":"string","description":"Human-readable catalog description."},"presets":{"type":"array","description":"Named teams of agents.","items":{"type":"object","required":["id","name","description","agentCount","answerAgentId","agents"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"agentCount":{"type":"integer","minimum":0},"answerAgentId":{"type":"string"},"agents":{"type":"array","items":{"type":"object","required":["id","name","expertise","tier","consumes"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"expertise":{"type":"string"},"tier":{"type":"string"},"consumes":{"type":"array","items":{"type":"string"}}}}}}}}}},"ChatRequest":{"type":"object","description":"A chat request. Answer modes: single (default), multiAgent (draft -> critique -> refine on one model), team (a curated roster of role-agents) and compare (one question, several models). team and multiAgent together are refused 400; compare with team or multiAgent is refused 400.","required":["modelId","messages"],"additionalProperties":false,"properties":{"modelId":{"type":"string","minLength":1,"maxLength":160,"description":"Model id from the governed registry. Unknown ids are refused 400; models above the caller's plan 403."},"messages":{"type":"array","minItems":1,"maxItems":100,"description":"Conversation so far, oldest first. Ignored when itemRisk is present (see itemRisk).","items":{"$ref":"#/components/schemas/ChatMessage"}},"webSearch":{"type":"boolean","description":"Request native web search; honored only when the chosen model advertises the capability."},"multiAgent":{"type":"boolean","description":"Review mode: draft, critique and refine with the same model."},"team":{"type":"object","description":"Team mode. agentModels is a proposal only: every entry is re-checked against the caller's plan before the stream opens.","required":["teamId"],"additionalProperties":false,"properties":{"teamId":{"type":"string","minLength":1,"maxLength":64,"description":"A curated team id; unknown ids are refused 400."},"agentModels":{"type":"object","description":"Optional per-agent model override: agent id -> model id.","propertyNames":{"type":"string","maxLength":64},"additionalProperties":{"type":"string","minLength":1,"maxLength":160}}}},"compare":{"type":"object","description":"Compare mode. Duplicate ids are collapsed before the plan and daily-limit checks; every model must be in the caller's plan or the whole request is refused.","required":["modelIds"],"additionalProperties":false,"properties":{"modelIds":{"type":"array","minItems":2,"maxItems":4,"items":{"type":"string","minLength":1,"maxLength":160}}}},"itemRisk":{"type":"object","description":"Agent-Runner item execution only. When present, the server re-classifies the item and may refuse it (403), and the prompt sent to the model is rendered from kind/title/body alone - messages is not sent.","required":["kind","title","approved"],"additionalProperties":false,"properties":{"kind":{"type":"string","enum":["roadmap","task","checklist","blocker","approval","evidence"]},"title":{"type":"string","minLength":1,"maxLength":2000},"body":{"type":["string","null"],"maxLength":20000},"approved":{"type":"boolean"}}}}},"ChatMessage":{"type":"object","description":"One conversation turn.","required":["role","content"],"properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string","maxLength":200000},"images":{"type":"array","maxItems":4,"description":"Optional inline images for this turn.","items":{"$ref":"#/components/schemas/ChatImage"}}}},"ChatImage":{"type":"object","description":"One inline image attached to a message.","required":["mediaType","dataBase64"],"properties":{"mediaType":{"type":"string","enum":["image/png","image/jpeg","image/webp"]},"dataBase64":{"type":"string","minLength":1,"maxLength":6000000,"description":"Base64 image bytes with no data: prefix (about 4.5 MB of binary at the cap)."}}},"ChatStreamEvent":{"description":"One line of the application/x-ndjson response body: a JSON object followed by a newline. A normal stream ends with exactly one terminal event: done, aborted or error.","oneOf":[{"type":"object","description":"A phase (or agent, or compared model) starts on a channel.","required":["type","channel"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"phase"},"channel":{"type":"string","description":"Which part of the answer the event belongs to: \"answer\" (the final answer in every mode), \"draft\" / \"critique\" (multiAgent), an agent id (team; the answering agent reports on \"answer\"), or \"compare:<modelId>\" (compare)."},"label":{"type":"string","description":"Display label (team agent name or compared model label)."},"modelId":{"type":"string","description":"The model answering on this channel (team and compare)."}}},{"type":"object","description":"A chunk of model text for a channel.","required":["type","channel","text"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"delta"},"channel":{"type":"string","description":"Which part of the answer the event belongs to: \"answer\" (the final answer in every mode), \"draft\" / \"critique\" (multiAgent), an agent id (team; the answering agent reports on \"answer\"), or \"compare:<modelId>\" (compare)."},"text":{"type":"string"}}},{"type":"object","description":"One team agent or one compared model failed; the run continues. The message is a fixed, safe string (never provider exception text).","required":["type","channel","message"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"step_error"},"channel":{"type":"string","description":"Which part of the answer the event belongs to: \"answer\" (the final answer in every mode), \"draft\" / \"critique\" (multiAgent), an agent id (team; the answering agent reports on \"answer\"), or \"compare:<modelId>\" (compare)."},"message":{"type":"string"}}},{"type":"object","description":"The chosen model's provider failed before answering and another model in the caller's plan is answering instead. Labels only, never the error text.","required":["type","channel","fromLabel","toLabel","modelId"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"failover"},"channel":{"type":"string","description":"Which part of the answer the event belongs to: \"answer\" (the final answer in every mode), \"draft\" / \"critique\" (multiAgent), an agent id (team; the answering agent reports on \"answer\"), or \"compare:<modelId>\" (compare)."},"fromLabel":{"type":"string"},"toLabel":{"type":"string"},"modelId":{"type":"string","description":"Id of the model now answering."}}},{"type":"object","description":"Terminal: the run completed.","required":["type"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"done"}}},{"type":"object","description":"Terminal: the client aborted the request (the aborted calls are still metered).","required":["type"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"aborted"}}},{"type":"object","description":"Terminal: the run failed after the stream opened. The HTTP status is still 200; this event carries the failure.","required":["type","message","reference"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"error"},"message":{"type":"string","description":"A fixed, safe message (or a team's misconfiguration reason)."},"reference":{"type":"string","description":"Opaque support reference to quote to support (the request id for a misconfigured team)."},"retryAfterSec":{"type":"integer","minimum":1,"maximum":86400,"description":"Present only when the AI provider was rate-limited or briefly unavailable and said how long to wait (its Retry-After, whole seconds rounded up, at most one day); the message then already says how long."}}}]},"VideoSubmitRequest":{"type":"object","description":"A video-generation job. The prompt is screened by the safety pre-filter before any provider call.","required":["prompt"],"additionalProperties":false,"properties":{"prompt":{"type":"string","minLength":1,"maxLength":2000,"description":"Length is checked after trimming whitespace."},"aspect":{"type":"string","enum":["1:1","16:9","9:16","4:3","3:4"]},"durationSeconds":{"type":"integer","minimum":1,"maximum":30,"description":"Requested clip length in whole seconds, 1-30. A length outside that range is refused 422 rather than shortened. The provider renders the nearest length it supports."},"model":{"type":"string","minLength":1,"maxLength":120,"description":"Model id within the provider (checked after trimming). A model the provider does not declare is refused 400."}}},"VideoJobHandle":{"type":"object","description":"Handle for an accepted job. Poll with GET ?jobId=<jobId>&provider=<provider>.","required":["jobId","provider","model"],"properties":{"jobId":{"type":"string"},"provider":{"type":"string"},"model":{"type":"string"}}},"VideoJobState":{"type":"object","description":"Current state of a job. result is present only when completed; error only when failed.","required":["status"],"properties":{"status":{"type":"string","enum":["queued","processing","completed","failed"]},"progress":{"type":"number","minimum":0,"maximum":1,"description":"Fractional progress, when the provider reports it."},"error":{"type":"string","description":"A fixed, general message (status failed only). The provider's own failure text is not returned."},"result":{"type":"object","required":["provider","model","video","createdAt"],"properties":{"provider":{"type":"string"},"model":{"type":"string"},"createdAt":{"type":"string","format":"date-time","description":"When the job was submitted."},"video":{"type":"object","required":["url","mimeType"],"properties":{"url":{"type":"string","description":"Where to fetch the video: the platform's own GET /api/design/video/content address for this job, relative to the serving origin. Never a provider address."},"mimeType":{"type":"string","description":"The media type the provider declared for the render."}}}}}}},"StripeWebhookEvent":{"type":"object","description":"The Stripe event envelope. Only these fields are validated; the rest of the payload is ignored.","required":["id","object","type","created","livemode","data"],"properties":{"id":{"type":"string","minLength":1},"object":{"type":"string","const":"event"},"type":{"type":"string","minLength":1,"description":"Any Stripe event type; none is refused."},"created":{"type":"number"},"livemode":{"type":"boolean"},"api_version":{"type":["string","null"]},"data":{"type":"object","required":["object"],"properties":{"object":{"type":"object","additionalProperties":true},"previous_attributes":{"type":"object","additionalProperties":true}}}}}}}}