{"openapi":"3.1.0","info":{"title":"Synoptic Customer API","version":"1.0.0","description":"Token-authenticated programmatic surface for Synoptic: dashboards (CRUD + widgets + preview + render), ask and direct context retrieval. Authenticate with an org-scoped API key (Settings → API Keys) as a bearer token. A key carries scopes — the access grant — and is org-scoped: every request derives its org ONLY from the key, and sees every record of the workspace's workspace connections, and none of anyone's personal connections.\n\nRate limits (per organization, per minute): 120 reads, 120 writes, 1,000 ingest calls, 10 pipeline-backed calls (ask / preview / render). Over the limit returns 429 with Retry-After. These are FLOORS, not exact ceilings: counters are held per web task and requests are not pinned to a task, so a caller is always allowed at least the stated number per minute and may in practice be allowed a small multiple of it. Do not build a client that depends on the extra headroom — it varies with how the service is scaled. Honour Retry-After instead.\n\nPer-key query limits (exact, shared across the service): each key may make 600 context searches plus asks per minute and run 20 at once. Over the rate returns 429 rate_limited; a 21st call while 20 run returns 429 concurrency_limited. Both carry Retry-After. A workspace can ask for higher limits."},"servers":[{"url":"/api/v1"}],"components":{"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","bearerFormat":"sk_synoptic_*"}},"schemas":{"IngestDocument":{"type":"object","additionalProperties":false,"required":["id","text","timestamp"],"properties":{"id":{"type":"string","minLength":1,"maxLength":512},"text":{"type":"string","minLength":1,"maxLength":1000000},"timestamp":{"type":"string","format":"date-time"},"metadata":{"description":"Any JSON, at most 64 KB. metadata.title and metadata.url are shown with results."}}},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Stable machine code. 400 invalid_request; 409 conflict (idempotency_conflict when an Idempotency-Key or call id is reused with different content); 413 payload_too_large."},"message":{"type":"string"},"request_id":{"type":"string"}},"required":["code","message","request_id"]}},"required":["error"]},"Widget":{"type":"object","required":["type","prompt"],"properties":{"type":{"type":"string","enum":["number","over_time","breakdown","share","over_time_by_group","table","narrative","status_kpi","before_after","timeline","quote","checklist","param_block"]},"prompt":{"type":"string","description":"The widget's NL subject. Untrusted free text — stored, never interpreted."},"title":{"type":"string","maxLength":80,"description":"Optional short heading for the card. Omit and the heading is derived from the subject. Display-only — never compiled or executed, so it cannot change what the widget computes."},"spec":{"type":"object","description":"Deliverable widgets only (status_kpi/timeline/checklist/...). Display-only labels, never executed."},"params":{"type":"object","properties":{"metric":{"type":"string"},"dimension":{"type":"string"},"time_bucket":{"type":"string"}}}}},"Dashboard":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"widgets":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/Widget"}},"dateControl":{"type":"object"}}}}},"security":[{"ApiKey":[]}],"paths":{"/context":{"post":{"summary":"Retrieve source evidence directly without Ask","description":"Scope: ask. Search uses the shared pipeline rate limit; other operations use the read tier. The key chooses the workspace and sees workspace connections only. No query_id is required. Omitted operation means search. Returns source text, provenance, versions, cost and coverage. Follow next_cursor with next_page_offset, and next_content_offset with source_version for full-source reads. Partial coverage is not evidence of absence. For calendar event browsing, set participant to an email or any.\n\nLochless workspaces: limit is results per page, default 50, up to 100. The response is results, total_results (every result the query has, across all pages), next_cursor (pass it back as cursor), tokens_scanned and request_id. Only the first page is charged; later pages of the same query are free. Filters (search and browse): from / to (ISO 8601, on the document's own timestamp; from inclusive, to exclusive; a YYYY-MM-DD date alone is that whole UTC day) and metadata ({key: value} exact match; a list value = any of). Only matching documents are read, scanned and billed. dry_run: true (search) returns documents_matched, documents_ready, tokens_to_scan and price_usd without scanning; free, rate-limited like a query. A dry run also returns read_limit_usd and complete (whether that limit reads every matching document).\n\nRead limit (search): read_limit_usd caps what one call may cost, default 1. Over it, the best-matching documents are read first, and only what was read is billed. coverage is {mode, documents_read, documents_total, complete}. When complete is false the response has continue_cursor: send the same request with it to read the next best-matching documents, up to read_limit_usd again; no document is read twice. next_cursor pages stay free. 409 conflict: the documents changed since the first read; run the query again.\n\nRead mode (search): mode is fast or complete. fast reads only the best-matching results (a lookup: one fact or answer), quicker and cheaper, and bills only what it read; it may miss matches. complete reads every matching document, up to read_limit_usd. Without mode the question decides. coverage.mode says which ran; a fast read has no continue_cursor (send mode: complete to read every match).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","anyOf":[{"oneOf":[{"type":"object","properties":{"operation":{"type":"string","const":"search"},"query":{"type":"string","minLength":1,"maxLength":4000},"sources":{"description":"Optional sources for this subquery, within the user's task boundaries.","anyOf":[{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":128}},{"type":"null"}]},"limit":{"default":5,"type":"integer","minimum":1,"maximum":20},"cursor":{"type":"string","maxLength":2000},"page_offset":{"type":"integer","minimum":0,"maximum":9007199254740991},"time_field":{"default":"activity_at","type":"string","enum":["activity_at","event_start","source_updated_at","indexed_at"]},"since":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"until":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"order":{"default":"desc","type":"string","enum":["asc","desc"]},"database_id":{"type":"string","minLength":1,"maxLength":128}},"required":["operation","query","limit","time_field","order"],"additionalProperties":false},{"type":"object","properties":{"operation":{"type":"string","const":"browse"},"sources":{"description":"Optional sources for this subquery, within the user's task boundaries.","anyOf":[{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":128}},{"type":"null"}]},"limit":{"default":5,"type":"integer","minimum":1,"maximum":20},"cursor":{"type":"string","maxLength":2000},"page_offset":{"type":"integer","minimum":0,"maximum":9007199254740991},"time_field":{"default":"activity_at","type":"string","enum":["activity_at","event_start","source_updated_at","indexed_at"]},"since":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"until":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"order":{"default":"desc","type":"string","enum":["asc","desc"]},"participant":{"description":"For timed calendar events: me (default), any, or an attendee email.","type":"string","minLength":1,"maxLength":320},"scan_after":{"type":"object","properties":{"at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"id":{"type":"string","minLength":1,"maxLength":2048}},"required":["at","id"],"additionalProperties":false}},"required":["operation","limit","time_field","order"],"additionalProperties":false},{"type":"object","properties":{"operation":{"type":"string","const":"read"},"document_id":{"type":"string","minLength":1,"maxLength":2048},"connection_id":{"type":"string","minLength":1,"maxLength":128},"source_version":{"type":"string","maxLength":128},"content_offset":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["operation","document_id","connection_id","content_offset"],"additionalProperties":false},{"type":"object","properties":{"operation":{"type":"string","const":"database"},"database_id":{"type":"string","minLength":1,"maxLength":128},"sources":{"description":"Optional sources for this subquery, within the user's task boundaries.","anyOf":[{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":128}},{"type":"null"}]},"limit":{"default":5,"type":"integer","minimum":1,"maximum":20},"cursor":{"type":"string","maxLength":2000},"page_offset":{"type":"integer","minimum":0,"maximum":9007199254740991},"record_id":{"type":"string"},"revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"source_version":{"type":"string"},"content_offset":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["operation","database_id","limit"],"additionalProperties":false}]},{"type":"object","properties":{"query":{"type":"string","minLength":1,"maxLength":4000},"sources":{"description":"Optional sources for this subquery, within the user's task boundaries.","anyOf":[{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":128}},{"type":"null"}]},"limit":{"default":5,"type":"integer","minimum":1,"maximum":20},"cursor":{"type":"string","maxLength":2000},"page_offset":{"type":"integer","minimum":0,"maximum":9007199254740991},"time_field":{"default":"activity_at","type":"string","enum":["activity_at","event_start","source_updated_at","indexed_at"]},"since":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"until":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"order":{"default":"desc","type":"string","enum":["asc","desc"]},"database_id":{"type":"string","minLength":1,"maxLength":128}},"required":["query","limit","time_field","order"],"additionalProperties":false}]},"example":{"query":"Which support problems block deployment?","sources":["gong","pylon"]}}}},"responses":{"200":{"description":"Source records, continuation fields, cost_usd, coverage and tokens_scanned (your text this call read, each piece once; 0 for browse and read). No composed answer."},"400":{"description":"Invalid context request or forbidden identity fields"},"401":{"description":"Missing, invalid, revoked or expired API key"},"402":{"description":"spend_cap_reached (monthly spend cap reached) or add_credit (balance used up; top up in the console)"},"403":{"description":"Missing ask scope or unavailable source scope"},"409":{"description":"conflict: source, access or continuation changed; restart the read"},"422":{"description":"Unsupported filter, including calendar me for a workspace key"},"429":{"description":"Rate limit exceeded"},"502":{"description":"Context temporarily unavailable"}}}},"/recorder":{"get":{"summary":"Verify a legacy desktop recorder key and return its workspace","description":"Legacy Mini Recorder compatibility endpoint; new setup uses Synoptic Meeting Recorder in Meetings. Available only to Synoptic - Prod. Scope: recordings:write. Returns workspace_id, optional workspace_name, source=mini-recorder, visibility=workspace, audio_upload=true. Creates no source.","responses":{"200":{"description":"Verified recorder destination"},"404":{"description":"Recorder unavailable for this workspace"}}},"post":{"summary":"Queue a call transcript from legacy Synoptic Mini Recorder","description":"Legacy desktop compatibility endpoint; new setup uses Synoptic Meeting Recorder in Meetings. Available only to Synoptic - Prod; other workspaces receive 404. Scope: recordings:write. Maximum JSON body: 4 MB. One source per API key; workspace-visible. Same call_id and content safely replay; different content returns 409. A 202 confirms durable receipt, not completed indexing. Transcript-only endpoint; existing audio clients use /recorder/audio.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["call_id","recorded_at","title","text"],"properties":{"call_id":{"type":"string","format":"uuid"},"recorded_at":{"type":"string","format":"date-time"},"title":{"type":"string","minLength":1,"maxLength":160},"text":{"type":"string","minLength":1},"language":{"type":"string","maxLength":40}}}}}},"responses":{"202":{"description":"Call durably queued for indexing"},"409":{"description":"idempotency_conflict (call identity conflict) or conflict (paused source)"},"413":{"description":"payload_too_large"}}}},"/recorder/audio":{"post":{"summary":"Store legacy desktop call audio and queue server transcription","description":"Legacy Mini Recorder compatibility endpoint; new setup uses Synoptic Meeting Recorder in Meetings. Synoptic - Prod only; recordings:write. M4A audio, at most 512 MB. Workspace comes only from the key. An immutable call ID and recording date make retries safe. A 202 confirms audio in account S3 raw data; server transcription, naming, and indexing follow asynchronously.","parameters":[{"name":"call_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"recorded_at","in":"query","required":true,"schema":{"type":"string","format":"date-time"}}],"requestBody":{"required":true,"content":{"audio/mp4":{"schema":{"type":"string","format":"binary"}}}},"responses":{"202":{"description":"Audio durably stored"},"409":{"description":"idempotency_conflict (identity conflict) or conflict (paused source)"},"413":{"description":"payload_too_large: audio exceeds 512 MB"}}},"get":{"summary":"Read a legacy desktop call's server processing status","description":"Legacy Mini Recorder compatibility endpoint. Scope: recordings:write. Requires call_id and recorded_at; optional connection_id resumes polling after key rotation within the same workspace. Returns processing, transcribed, no_speech, or failed, plus a generated title when ready. Transcribed does not certify completed indexing.","parameters":[{"name":"call_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"recorded_at","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"connection_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Processing state and optional topic/date title"},"404":{"description":"Call unavailable"}}}},"/ingest":{"post":{"summary":"Queue documents for indexing","description":"Scope: ingest. One document, or {\"documents\": [...]} with 1 to 100. Each needs id (1-512 chars), text (up to 1,000,000 chars) and timestamp (ISO 8601 with a timezone; it dates the document). metadata is optional JSON (64 KB). The same id replaces. Body at most 25 MB. A 202 means durably queued, not indexed: indexing runs in the background. Lochless workspaces: indexing costs a price per page (3,000 characters of text, rounded up, at least 1 per document), charged from the balance once when the document is ready; a failed document is free, and re-sending the same text is free. At zero balance or at the monthly spend cap the call answers 402, and documents already queued wait for a top-up. 429 queue_full when a million documents are pending.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/IngestDocument"},{"type":"object","additionalProperties":false,"required":["documents"],"properties":{"documents":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/IngestDocument"}}}}]},"example":{"documents":[{"id":"t-4411","text":"Charged twice for the March invoice.","timestamp":"2026-10-01T09:30:00Z","metadata":{"title":"Ticket 4411"}}]}}}},"responses":{"202":{"description":"{documents: [{id, status}]}"},"400":{"description":"Invalid documents"},"401":{"description":"Missing, invalid, revoked or expired API key"},"402":{"description":"Lochless workspaces: spend_cap_reached (monthly spend cap reached) or add_credit (balance used up; top up in the console)"},"403":{"description":"Missing ingest scope"},"409":{"description":"conflict: ingest paused for this workspace"},"413":{"description":"payload_too_large: body or document too large"},"429":{"description":"Rate limited, or queue_full; honour Retry-After"}}}},"/docs/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"The document id sent to /ingest, percent-encoded","schema":{"type":"string","maxLength":512}}],"get":{"summary":"Document status","description":"Scope: ingest. Returns id, status (queued, indexing, ready or failed), timestamp, queued_at, updated_at, retries_left and last_error. A document whose indexing fails is retried on its own, up to 3 more times (about 5 minutes, 30 minutes and 2 hours later); while it waits it reads queued, with last_error saying why the last try failed. After the last retry it reads failed, with error {code, message}. Sending a failed document again, even unchanged, retries it at once. Indexing is charged only when a document is ready.","responses":{"200":{"description":"Document status"},"404":{"description":"No such document in this workspace"}}},"delete":{"summary":"Delete a document","description":"Scope: ingest. Removes the document; it leaves search and /ask results at once.","responses":{"200":{"description":"{id, status: deleted}"},"404":{"description":"No such document in this workspace"}}}},"/ask":{"post":{"summary":"Grounded answer over the org's account-level corpus","description":"Scope: ask. Rate tier: pipeline. ask, dashboards:read, dashboards:write, datasets:write, recordings:write, ingest are the available scopes. Lochless workspaces: /ask runs the same search as /context (from, to, metadata, read_limit_usd, mode; every date unless from/to narrow it), then writes a short answer from the results it kept. The response is answer (cites its evidence as [1], [2]), evidence (only the cited results: n, document_id, date, metadata, text, score), tokens_scanned, coverage {mode, documents_read, documents_total, complete} and request_id. read_limit_usd (default 1) caps what the call may cost, answer fee included: the search reads up to read_limit_usd less the fee, best-matching documents first, and a limit at or below the fee is a 400. Billed as the search's tokens scanned plus the answer fee; a call that writes no answer is not billed. A call still running after about 15 s answers 200 at once and keeps the connection open with whitespace until its JSON is ready; an error after that point is the error envelope in that body. dry_run returns documents_matched, tokens_to_scan, price_usd (what the call would cost: the scan within the limit plus the answer fee), read_limit_usd and complete, for free.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string"},"since":{"type":["string","null"],"format":"date","description":"Lower bound on document date. Omit for the workspace default range (about one month); null searches everything Synoptic holds."},"until":{"type":["string","null"],"format":"date","description":"Upper bound on document date."}}}}}},"responses":{"200":{"description":"answer + references + a formatted text block + `scope` (the range actually read: { since, until, scanned, total, complete, bounded_by }) + `tokens_scanned` (your text this answer read, each piece once)"},"400":{"description":"missing query, or a since/until that is neither a YYYY-MM-DD date nor null","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"missing/invalid/expired key"},"402":{"description":"spend_cap_reached (monthly spend cap reached) or add_credit (balance used up; top up in the console)"},"403":{"description":"key lacks the ask scope"},"429":{"description":"rate limited — see info.description; honour Retry-After"}}}},"/dashboards":{"get":{"summary":"List dashboards (scope: dashboards:read)","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","maximum":100,"default":25}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"{ data: Dashboard[], next_cursor: string|null }"},"401":{},"403":{},"429":{}}},"post":{"summary":"Create a dashboard (scope: dashboards:write)","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Dashboard"}}}},"responses":{"201":{"description":"{ id }"},"400":{"description":"invalid body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{},"403":{},"409":{"description":"idempotency_conflict: Idempotency-Key reused with a different body"},"429":{}}}},"/dashboards/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"summary":"Fetch a dashboard (scope: dashboards:read)","responses":{"200":{},"404":{}}},"patch":{"summary":"Replace name/widgets/dateControl (scope: dashboards:write)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Dashboard"}}}},"responses":{"200":{"description":"{ ok: true }"},"400":{},"404":{}}},"delete":{"summary":"Delete a dashboard (scope: dashboards:write)","responses":{"200":{"description":"{ ok: true }"}}}},"/dashboards/{id}/render":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"summary":"Compute all widgets over one snapshot (scope: dashboards:read)","description":"Long renders keep the connection warm: the 200 body may begin with whitespace keepalives (newlines, one every ~20s) before the single JSON document — insignificant leading whitespace per RFC 8259, skipped by every standard JSON parser, so res.json()/jq work unchanged. A failure after the response has started arrives as the standard error envelope ({ error: { code, message, request_id } }) in that body; check for an `error` key, not only the HTTP status.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"asOf":{"type":"string","format":"date"}}}}}},"responses":{"200":{"description":"{ widgetResults, layouts, status, ... }"},"404":{}}}},"/dashboards/{id}/render/stream":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"summary":"SSE per-widget render for slow canvases (scope: dashboards:read)","responses":{"200":{"description":"text/event-stream of meta / widget / done frames"},"404":{}}}},"/widgets/preview":{"post":{"summary":"Compile + run one widget, persist nothing (scope: dashboards:write)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Widget"}}}},"responses":{"200":{"description":"{ widget, result, chosen, options, interpretation, ... }"},"400":{}}}}}}