{"schema_version":"1","title":"Decosa demo API — contract v0 (2026-09-23)","base_url":"https://api.decosa.ai","source":"/api/contract.md","reference":"/docs/api","auth":{"hosted_keys":"Authorization: Bearer dk_… (create one at /account/keys)","demo_sessions":"POST /demo/session → token; then Authorization: Bearer <token> (WebSockets: ?token=)"},"note":"A route index extracted from the markdown contract. The markdown is the source of truth for bodies, events and errors.","routes":[{"method":"POST","path":"/demo/session","section":"Auth: demo sessions","summary":"`POST /demo/session` with body `{ \"vertical\": \"<id>\" }` returns `{ \"token\": \"<opaque>\", \"expires_at\": <unix>, \"budget\": { \"seconds_audio\": 300, \"llm_tokens\": 20000 } }`."},{"method":"GET","path":"/healthz","section":"Auth: demo sessions","summary":"Rate limits: {{DEMO_SESSIONS_PER_HOUR}} sessions per network per hour, {{DEMO_SESSIONS_ALL_HOUR}} per hour in total (`GET /healthz` reports both under `demo_sessions`). Global concurrency cap: live audio sessions ≤ 4 (configurable). When over the cap, return 429 with `Retry-After`."},{"method":"WS","path":"/ws/live","section":"Live audio verticals (clinical, sales, field, translate)","summary":"`WS /ws/live?vertical=<id>&token=<t>[&lang=<src>&target=<dst>]` (lang and target apply to `translate` only)"},{"method":"POST","path":"/demo/replay","section":"Live audio verticals (clinical, sales, field, translate)","summary":"Text fallback, for demos without a mic: `POST /demo/replay` with `{ \"vertical\": \"<id>\", \"script_id\": \"<id>\" }` streams the same event types over SSE, using a canned audio script run through the REAL pipeline, so the output is live."},{"method":"POST","path":"/v1/chat/completions","section":"Code assistant","summary":"`POST /v1/chat/completions` is OpenAI-compatible and streams (`stream:true`, SSE). Model `qwen3.8-27b`, bearer = the demo token. Each response carries header `x-decosa-receipt: <completion id>`. The final SSE chunk includes `\"receipt\":{...}` in the same shape as the WS receipt event."},{"method":"GET","path":"/studio/gallery","section":"Studio","summary":"`GET /studio/gallery` returns `[{ \"id\", \"kind\": \"music\"|\"image\"|\"video\", \"title\", \"prompt\", \"model\", \"url\", \"seed\", \"duration_s\"? }]`. Pre-rendered outputs are static files served under `/studio/media/...`."},{"method":"POST","path":"/studio/jobs","section":"Studio","summary":"`POST /studio/jobs` with body `{ \"kind\", \"prompt\", \"params\":{} }` returns `{ \"job_id\", \"status\":\"queued\", \"position\": N }`."},{"method":"GET","path":"/studio/jobs/{id}","section":"Studio","summary":"`GET /studio/jobs/{id}` returns `{ \"status\": \"queued\"|\"running\"|\"done\"|\"failed\", \"url\"?: ... }`."},{"method":"GET","path":"/demo/recordings","section":"Watch (recorded sessions)","summary":"`GET /demo/recordings` returns `[{ \"id\", \"vertical\", \"title\", \"duration_s\", \"events_url\", \"video_url\"? }]`."},{"method":"GET","path":"/receipts/{id}","section":"Receipts","summary":"`GET /receipts/{id}` returns `{ \"id\", \"model\", \"weights_root\", \"request_hash\", \"output_hash\", \"provider\": {\"miner_id\",\"pubkey\",\"sig\"}, \"gateway\": {\"pubkey\",\"sig\"}, \"proof\": {\"format\",\"verified\":true}, \"checks\": [{\"name\",\"ok\",\"detail\"}] }`. Read-only and public. It proxies our gateway's receipt data "},{"method":"GET","path":"/demo/scripts","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"New `GET /demo/scripts` (no token) lists them as `[{id, vertical, title, lang, target, duration_s, has_audio}]`. Recording ids equal script ids. `events_url` is a path relative to the API base."},{"method":"GET","path":"/v1/models","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"Code assistant: also `GET /v1/models`. Any valid demo token works. With `stream:true` the server gets the completion from the gateway's metered route, then re-emits it as SSE chunks, because the gateway's streaming route settles a receipt but does not return one. So time-to-first-token equals the fu"},{"method":"POST","path":"/v1/keys","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`POST /v1/keys`"},{"method":"GET","path":"/v1/keys/{key_id}","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /v1/keys/{key_id}` → `{key_id, label, plan, created, revoked, revoked_at, limits, today: {day, llm_tokens, audio_seconds, requests, llm_tokens_left, audio_seconds_left}, history: [last 30 days]}`. Days are UTC."},{"method":"DELETE","path":"/v1/keys/{key_id}","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`DELETE /v1/keys/{key_id}` → `{key_id, revoked: true}`. Revocation is immediate, and later uses get 401."},{"method":"GET","path":"/studio/policy","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /studio/policy` returns the rules as text and the report categories."},{"method":"GET","path":"/studio/report","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /studio/report?item=|job=` describes the form."},{"method":"POST","path":"/studio/report","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`POST /studio/report` takes `{item | job, category, details?, contact?}`."},{"method":"GET","path":"/attest/signing-key","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /attest/signing-key` (no token) → `{scheme:\"ed25519\", pubkey, key_id, name, created, signs:[domains], note, llm_route, receipts}`. 404 when signing is off."},{"method":"GET","path":"/attest/models","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /attest/models` (no token) → `{<served name>: {repo, revision, root, scheme:\"hf-files-v1\", files, bytes, source, manifest:[{path, sha256, size}]}}`. `root` = sha256 of the canonical JSON `{scheme, repo, revision, files}` (`scripts/model_root.py`; anyone can recompute it from the Hub's per-file "},{"method":"POST","path":"/record/verify","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"*5. Verification.* `POST /record/verify` (no token; body = the record, or `{record}`; up to `DECOSA_RECORD_MAX_BYTES`, 8 MiB; 60 per minute per address) → `{ok, checks:[{name, ok, detail}], bad:[{seq, what, problems}], first_bad, summary, session, signer, count, issued_here}`. Checks: `entries` (eac"},{"method":"GET","path":"/provenance/status","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /provenance/status` → `{receipts, receipt_pubkey, c2pa, c2pa_library, c2pa_issuer:{subject, issuer, not_after, root_sha256, self_issued_root}, c2pa_trust, c2pa_note, watermark:{enabled, scheme, kinds}, voice_cloning:\"off\", counts, weights_digests}`."},{"method":"GET","path":"/provenance/signing-key","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /provenance/signing-key` → `{receipt:{scheme, pubkey, key_id, domains, same_key_as:\"/attest/signing-key\"}, c2pa:{issuer, trust, note, dev_ca_pem}}`."},{"method":"POST","path":"/provenance/check","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`POST /provenance/check?name=<file name>` with the raw file as the body (any content type; up to `DECOSA_PROVENANCE_MAX_UPLOAD_MB`, 50; 60 per hour per address, `DECOSA_PROVENANCE_CHECKS_PER_HOUR`). The file is checked in memory and not stored. → `{sha256, bytes, mime, checked_at, credential:{presen"},{"method":"GET","path":"/provenance/receipts/{rr_id}","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /provenance/receipts/{rr_id}` → `{id, receipt, verification:{ok, pubkey_pinned, detail}, credential_url, gateway:null, note, consent?}`."},{"method":"GET","path":"/provenance/samples","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /provenance/samples` → `[{id, file, url, description}]` (files under `/studio/media/provenance-samples/`)."},{"method":"POST","path":"/provenance/consent-check","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`POST /provenance/consent-check` `{kind, consent_id?, likeness?}` → `{allowed:true, consent|null, note}` or `{allowed:false, category, reason, consent?}`. Dry run, nothing rendered."},{"method":"POST","path":"/provenance/consents","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`POST /provenance/consents` (API key `dk_…` or the admin secret; demo tokens get 401) `{subject_label (pseudonym), clip_sha256, statement_sha256?, scope:{likeness:[\"voice\"|\"face\"], kinds:[\"voice\"|\"music\"|\"image\"|\"video\"], note?}, expires_in_days (1–3650, default 365)}` → 201 `{record, status}`. Reco"},{"method":"GET","path":"/provenance/consents/{cr_id}","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /provenance/consents/{cr_id}` → `{id, state:\"active\"|\"expired\"|\"revoked\"|\"invalid\", signature_ok, key_pinned, scope, subject_label, clip_sha256, granted_at, expires_at, revoked_at, record_sha256}`."},{"method":"POST","path":"/provenance/consents/{cr_id}/revoke","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`POST /provenance/consents/{cr_id}/revoke` (API key or admin) `{reason?}` → the status. Revocation is signed (domain `decosa.consent-revocation.v1`)."},{"method":"GET","path":"/audit/signing-key","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /audit/signing-key` (no token) → `{alg: \"ed25519\", pubkey, key_id, domain, canonical}`. A dedicated auditor key, a key file in the API's config directory or `DECOSA_AUDITOR_KEY`; `DECOSA_AUDITOR_CREATE_KEY=1` creates it on first start. Without a key the audit routes return 503."},{"method":"GET","path":"/audit/targets","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /audit/targets` (no token) → `[{id, label, claimed_model, kind: \"gateway\"|\"openai\", note, expect, available}]`: our own demo endpoints (`hosted`, `direct`, `swap`, `quant`), availability cached 10 s."},{"method":"GET","path":"/audit/references","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /audit/references`, `GET /audit/references/{id}` (no token) → the signed reference fixtures (`data/refs/<id>.json` + `.sig`, Ed25519 over `\"decosa.audit.reference.v1\\n\" + sha256(file)`)."},{"method":"GET","path":"/audit/references/{id}","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /audit/references`, `GET /audit/references/{id}` (no token) → the signed reference fixtures (`data/refs/<id>.json` + `.sig`, Ed25519 over `\"decosa.audit.reference.v1\\n\" + sha256(file)`)."},{"method":"POST","path":"/audit/runs","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`POST /audit/runs` (token) → SSE. Body `{target}` or `{base_url, model, claimed_model?, api_key?, context_tokens? (0–32000, default 12000), extra_body?}`."},{"method":"GET","path":"/audit/reports/{id}","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /audit/reports/{id}` (no token) → the signed report. `GET /audit/reports` → the 20 newest reports for demo targets (custom-endpoint reports are never listed). `POST /audit/verify {report}` → `{valid_signature, signed_by_this_auditor, auditor_pubkey}`."},{"method":"GET","path":"/audit/reports","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /audit/reports/{id}` (no token) → the signed report. `GET /audit/reports` → the 20 newest reports for demo targets (custom-endpoint reports are never listed). `POST /audit/verify {report}` → `{valid_signature, signed_by_this_auditor, auditor_pubkey}`."},{"method":"POST","path":"/audit/verify","section":"Changes (decosa-api v0.1.0, 2026-09-23)","summary":"`GET /audit/reports/{id}` (no token) → the signed report. `GET /audit/reports` → the 20 newest reports for demo targets (custom-endpoint reports are never listed). `POST /audit/verify {report}` → `{valid_signature, signed_by_this_auditor, auditor_pubkey}`."}]}