{"openapi":"3.1.0","info":{"title":"Energixer API","description":"Video hosting and processing as a service: resumable uploads, adaptive-streaming transcodes, public delivery, signed webhooks. Authenticate with your domain API key (`Authorization: Bearer ek_...`) unless an operation says otherwise. Upload (tus) and public delivery endpoints are documented in the guides at /docs.","version":"1.0.0"},"components":{"securitySchemes":{"domainKey":{"type":"http","scheme":"bearer","description":"Domain API key (ek_...)"}},"schemas":{}},"paths":{"/v1/health":{"get":{"tags":["meta"],"description":"Liveness probe. Always public.","responses":{"200":{"description":"Default Response"}}}},"/v1/domain":{"get":{"tags":["tenancy"],"description":"The calling domain: slug, config (with defaults resolved), quota limits, key last4. webhook_secret is never returned — only webhook_secret_set.","security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}}}}},"/v1/domain/config":{"patch":{"tags":["tenancy"],"description":"Merge-patch the per-domain config (renditions, outputs, caps, cors_origins, webhook settings). Unknown keys are rejected.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"max_file_bytes":{"type":"integer","minimum":1,"maximum":209715200},"max_duration_seconds":{"type":"integer","minimum":1,"maximum":900},"renditions":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","enum":["h264_720","h264_1080","av1_720","av1_1080"]}},"outputs":{"type":"object","additionalProperties":false,"properties":{"hls":{"type":"boolean"},"dash":{"type":"boolean"},"mp4_fallback":{"type":"boolean"},"og_thumbnail":{"type":"boolean"}}},"transcript":{"type":"boolean"},"moderation_frames":{"type":"boolean"},"max_concurrent_pending_per_uploader":{"type":"integer","minimum":1,"maximum":10},"cors_origins":{"type":"array","maxItems":10,"items":{"type":"string","pattern":"^https?://","maxLength":255}},"default_visibility":{"type":"string","enum":["private","public"]},"default_outro_slug":{"type":["string","null"],"pattern":"^[a-z0-9][a-z0-9_-]{0,63}$","maxLength":64},"webhook_url":{"type":["string","null"],"maxLength":500},"webhook_secret":{"type":["string","null"],"pattern":"^whsec_","maxLength":100}}}}}},"security":[{"domainKey":[]}],"responses":{"400":{"description":"Invalid config value","content":{"application/json":{"schema":{"description":"Invalid config value","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["validation_error"]}},"required":["error"]}}}},"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}}}}},"/v1/domain/rotate-key":{"post":{"tags":["tenancy"],"description":"Rotate this domain API key. The response is the only time the new key is shown; the old key stays valid for 1 hour.","security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}}}}},"/v1/limits":{"get":{"tags":["tenancy"],"description":"Effective upload limits for the calling identity (domain key or sandbox session): max file bytes and max duration, instance ceilings applied.","security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad key","content":{"application/json":{"schema":{"description":"Bad key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}}}}},"/v1/videos":{"post":{"tags":["videos"],"description":"Initiate an upload. Returns the tus endpoint plus a single-video upload token your device uses directly — the API key never ships client-side. Checks run in a fixed order: content type, size, duration, quotas (429 upload_quota_exceeded is distinct from rate limiting), pending slot, expired-upload throttle.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["content_type","file_size"],"properties":{"content_type":{"type":"string","minLength":1,"maxLength":100},"file_size":{"type":"integer","minimum":1,"maximum":209715200},"duration_seconds":{"type":"integer","minimum":1},"external_ref":{"type":"string","minLength":1,"maxLength":255},"uploader_ref":{"type":"string","minLength":1,"maxLength":255},"transcript":{"type":"boolean","description":"Ask for (or refuse) a transcript for THIS video, overriding the domain's `transcript` setting in either direction. Omit to use the domain setting, which is off by default. A worker with no whisper model reports transcript_status \"unavailable\" — the request is never a reason to fail the video."},"outro_slug":{"type":["string","null"],"pattern":"^[a-z0-9][a-z0-9_-]{0,63}$","maxLength":64,"description":"Append this domain’s outro to THIS video’s web.mp4 (ADR-22), overriding the domain’s default_outro_slug in either direction. Omit to use the domain setting; pass null to opt this video out of a domain that has one. The slug is resolved to an asset id HERE and pinned, so replacing the outro later does not change a video already in flight. Affects web.mp4 only — the HLS and DASH renditions never carry it. Sandbox sessions may not set this."},"visibility":{"type":"string","enum":["private","public"],"description":"Override the domain’s default_visibility for THIS video (ADR-21). Omit to use the domain setting, which is `private` by default. A private video needs a playback key on every delivery request; a public one is served to anyone holding its URL. Sandbox sessions may not set this."}}}}}},"security":[{"domainKey":[]}],"responses":{"400":{"description":"Failed validation checks 1–3, or an unresolvable outro_slug","content":{"application/json":{"schema":{"description":"Failed validation checks 1–3, or an unresolvable outro_slug","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["invalid_content_type","file_too_large","duration_too_long","outro_not_found"]}},"required":["error"]}}}},"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox session set visibility or outro_slug","content":{"application/json":{"schema":{"description":"Sandbox session set visibility or outro_slug","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}},"409":{"description":"Uploader already has a pending upload (existing_video_id included), or the named outro has never finished encoding","content":{"application/json":{"schema":{"description":"Uploader already has a pending upload (existing_video_id included), or the named outro has never finished encoding","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["concurrent_upload_exists","outro_not_ready"]}},"required":["error"]}}}},"429":{"description":"Quota exhausted or throttled — codes are deliberately distinct","content":{"application/json":{"schema":{"description":"Quota exhausted or throttled — codes are deliberately distinct","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["upload_quota_exceeded","too_many_incomplete_uploads","rate_limited"]}},"required":["error"]}}}},"503":{"description":"The domain’s storage backend is not working; no bytes are accepted","content":{"application/json":{"schema":{"description":"The domain’s storage backend is not working; no bytes are accepted","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["storage_unavailable"]}},"required":["error"]}}}}}},"get":{"tags":["videos"],"description":"This domain's videos, newest first, with optional state filter — the queue view the demo app renders. Keyset pagination via signed cursors; pass next_cursor back verbatim.","parameters":[{"schema":{"type":"string","enum":["pending_upload","uploaded","queued","locked","encoding","available","failed","timeout","expired","cancelled"]},"in":"query","name":"state","required":false},{"schema":{"type":"string","minLength":1,"maxLength":512},"in":"query","name":"cursor","required":false},{"schema":{"type":"integer","minimum":1,"maximum":100},"in":"query","name":"limit","required":false}],"security":[{"domainKey":[]}],"responses":{"400":{"description":"Tampered/invalid cursor","content":{"application/json":{"schema":{"description":"Tampered/invalid cursor","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["invalid_cursor"]}},"required":["error"]}}}},"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}}}}},"/v1/videos/{id}":{"get":{"tags":["videos"],"description":"Full video object: state, progress, outputs when available, failure reason. Poll ~10 s until a terminal state, or use webhooks. Foreign-tenant ids return 404, never 403.","parameters":[{"schema":{"type":"string","minLength":36,"maxLength":36},"in":"path","name":"id","required":true}],"security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"404":{"description":"Unknown or foreign video","content":{"application/json":{"schema":{"description":"Unknown or foreign video","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["video_not_found"]}},"required":["error"]}}}}}},"delete":{"tags":["videos"],"description":"Soft-delete: cancels any in-flight work, removes upload data immediately, reaps output files asynchronously, updates the storage gauge.","parameters":[{"schema":{"type":"string","minLength":36,"maxLength":36},"in":"path","name":"id","required":true}],"security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"404":{"description":"Unknown or foreign video","content":{"application/json":{"schema":{"description":"Unknown or foreign video","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["video_not_found"]}},"required":["error"]}}}}}}},"/v1/videos/{id}/visibility":{"patch":{"tags":["playback"],"description":"Make a video private or public. Private (the default) requires a playback key on every delivery request; public is served to anyone holding the URL. Takes effect within 60s — delivery reads visibility through a short-lived cache. Note that flipping public → private does NOT retract copies already cached downstream: a public response is cacheable for up to 24h, so shared caches and CDNs may keep serving it until that expires. Create videos private (the default) rather than relying on the flip if that matters.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["visibility"],"properties":{"visibility":{"type":"string","enum":["private","public"]}}}}}},"parameters":[{"schema":{"type":"string","minLength":36,"maxLength":36},"in":"path","name":"id","required":true}],"security":[{"domainKey":[]}],"responses":{"400":{"description":"Invalid visibility","content":{"application/json":{"schema":{"description":"Invalid visibility","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["validation_error"]}},"required":["error"]}}}},"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox session","content":{"application/json":{"schema":{"description":"Sandbox session","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}},"404":{"description":"Unknown or foreign video","content":{"application/json":{"schema":{"description":"Unknown or foreign video","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["video_not_found"]}},"required":["error"]}}}}}}},"/v1/videos/{id}/transcript":{"get":{"tags":["videos"],"description":"The transcript text, when one was produced. 200 with text: null whenever there is none — a video with no transcript is not a missing video, and status says which case you are in. The timed WebVTT lives at transcript_url and is served from the delivery plane like every other output — which means a private video’s captions need a playback key too (ADR-21), and a browser cannot put one on a <track src>. Fetch it and hand the player a blob URL. `language` is the ISO 639-1 code the transcript came out in — detected per video, and what a player passes to <track srclang>.","parameters":[{"schema":{"type":"string","minLength":36,"maxLength":36},"in":"path","name":"id","required":true}],"security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"404":{"description":"Unknown or foreign video","content":{"application/json":{"schema":{"description":"Unknown or foreign video","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["video_not_found"]}},"required":["error"]}}}}}}},"/v1/videos/{id}/cancel":{"post":{"tags":["videos"],"description":"Cancel a non-terminal video: frees the uploader slot, stops a running worker at its next heartbeat, removes upload data. Emits video.cancelled.","parameters":[{"schema":{"type":"string","minLength":36,"maxLength":36},"in":"path","name":"id","required":true}],"security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"404":{"description":"Unknown or foreign video","content":{"application/json":{"schema":{"description":"Unknown or foreign video","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["video_not_found"]}},"required":["error"]}}}},"409":{"description":"Already terminal","content":{"application/json":{"schema":{"description":"Already terminal","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["invalid_state"]}},"required":["error"]}}}}}}},"/v1/outros":{"post":{"tags":["outros"],"description":"Initiate an outro upload. Returns the same tus endpoint and single-asset upload token a video upload uses — the transfer is identical, only what happens afterwards differs: an outro is normalised to one mezzanine instead of an encoding ladder, is never delivered, and never appears in GET /v1/videos. Name the slug on an upload with outro_slug, or on the whole domain with default_outro_slug, and the worker appends it to that video’s web.mp4 (the Open Graph artifact) and to nothing else — HLS and DASH are untouched.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["slug","content_type","file_size"],"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9_-]{0,63}$","maxLength":64,"description":"How uploads name this outro. Unique per domain. Re-using an existing slug REPLACES it: a new asset id is minted and the slug repoints, while videos that already resolved the old id keep it. The previous asset keeps serving new uploads until the replacement finishes encoding, so rotating an outro never makes its slug unusable."},"content_type":{"type":"string","minLength":1,"maxLength":100},"file_size":{"type":"integer","minimum":1,"maximum":209715200},"duration_seconds":{"type":"integer","minimum":1},"label":{"type":"string","minLength":1,"maxLength":120,"description":"Human-readable name for your own console. Never rendered."}}}}}},"security":[{"domainKey":[]}],"responses":{"400":{"description":"Failed validation checks 1–3","content":{"application/json":{"schema":{"description":"Failed validation checks 1–3","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["invalid_content_type","file_too_large","duration_too_long"]}},"required":["error"]}}}},"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox session","content":{"application/json":{"schema":{"description":"Sandbox session","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}},"409":{"description":"This domain already holds the maximum number of outros","content":{"application/json":{"schema":{"description":"This domain already holds the maximum number of outros","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["outro_limit_reached"]}},"required":["error"]}}}},"429":{"description":"Daily byte quota exhausted","content":{"application/json":{"schema":{"description":"Daily byte quota exhausted","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["upload_quota_exceeded"]}},"required":["error"]}}}},"503":{"description":"The domain’s storage backend is not working","content":{"application/json":{"schema":{"description":"The domain’s storage backend is not working","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["storage_unavailable"]}},"required":["error"]}}}}}},"get":{"tags":["outros"],"description":"This domain’s outros, one entry per slug. Poll until state is \"available\" before naming a slug on an upload. No pagination: outros are capped at 50 per domain, so this is always the whole set. While a replacement is encoding, `state` describes the incoming asset and `serving_outro_id` names the one uploads still get.","security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox session","content":{"application/json":{"schema":{"description":"Sandbox session","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}}}}},"/v1/outros/{slug}":{"get":{"tags":["outros"],"description":"One outro by slug.","parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9_-]{0,63}$","maxLength":64},"in":"path","name":"slug","required":true}],"security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox session","content":{"application/json":{"schema":{"description":"Sandbox session","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}},"404":{"description":"No such slug on this domain","content":{"application/json":{"schema":{"description":"No such slug on this domain","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["outro_not_found"]}},"required":["error"]}}}}}},"delete":{"tags":["outros"],"description":"Delete an outro and free its slug. Videos that already resolved this outro are NOT failed: they pinned an asset id at initiate, and if the bytes are gone by the time they encode they simply ship without a tail card (outputs.web_mp4_outro_id reports what was actually used). Uploads naming the slug after this get outro_not_found.","parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9_-]{0,63}$","maxLength":64},"in":"path","name":"slug","required":true}],"security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox session","content":{"application/json":{"schema":{"description":"Sandbox session","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}},"404":{"description":"No such slug on this domain","content":{"application/json":{"schema":{"description":"No such slug on this domain","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["outro_not_found"]}},"required":["error"]}}}}}}},"/v1/videos/{id}/frames/{*}":{"get":{"tags":["videos"],"description":"Moderation frames for the domain’s own tooling. Never public.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"*","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/playback-keys":{"post":{"tags":["playback"],"description":"Mint a playback key for one end user. Send it as Authorization: Bearer on requests to /videos/:id/* for this domain’s private videos. Default TTL 1h, maximum 24h. Pass video_ids to narrow the key to specific videos. Transport is the Authorization header only — a browser can carry it on fetches your page makes (hls.js), but not on <img>, <track>, or a native <video src>, so posters and caption tracks must be fetched to a blob URL.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["viewer_ref"],"properties":{"viewer_ref":{"type":"string","minLength":1,"maxLength":255},"ttl_seconds":{"type":"integer","minimum":60,"maximum":86400},"video_ids":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","maxLength":36}}}}}}},"security":[{"domainKey":[]}],"responses":{"400":{"description":"Invalid mint request","content":{"application/json":{"schema":{"description":"Invalid mint request","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["validation_error"]}},"required":["error"]}}}},"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}}}}},"/v1/playback-keys/revoke":{"post":{"tags":["playback"],"description":"Revoke every outstanding playback key for one viewer_ref. Takes effect within 5 seconds. Keys minted after this call are unaffected, so re-issuing to the same viewer works immediately.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["viewer_ref"],"properties":{"viewer_ref":{"type":"string","minLength":1,"maxLength":255}}}}}},"security":[{"domainKey":[]}],"responses":{"400":{"description":"Invalid request","content":{"application/json":{"schema":{"description":"Invalid request","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["validation_error"]}},"required":["error"]}}}},"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox session","content":{"application/json":{"schema":{"description":"Sandbox session","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}},"409":{"description":"Denylist full","content":{"application/json":{"schema":{"description":"Denylist full","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["playback_revocation_limit"]}},"required":["error"]}}}}}}},"/v1/playback-keys/revoke-all":{"post":{"tags":["playback"],"description":"Invalidate every outstanding playback key for this domain by bumping its epoch. Takes effect within 5 seconds. This is also the escape hatch when the per-viewer denylist is full — it drains as its rows age out.","security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox session","content":{"application/json":{"schema":{"description":"Sandbox session","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}}}}},"/v1/webhook-deliveries":{"get":{"tags":["webhooks"],"description":"Webhook delivery log, newest first. status=failed means dead-lettered (six attempts exhausted); replay to retry.","parameters":[{"schema":{"type":"string","enum":["pending","failed","delivered"]},"in":"query","name":"status","required":false},{"schema":{"type":"string","minLength":1},"in":"query","name":"cursor","required":false},{"schema":{"type":"integer","minimum":1,"maximum":100},"in":"query","name":"limit","required":false}],"security":[{"domainKey":[]}],"responses":{"400":{"description":"Tampered/invalid cursor","content":{"application/json":{"schema":{"description":"Tampered/invalid cursor","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["invalid_cursor"]}},"required":["error"]}}}},"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}}}}},"/v1/webhook-deliveries/{id}/replay":{"post":{"tags":["webhooks"],"description":"Re-deliver a webhook (same delivery id — consumer idempotency is the contract). Grants exactly one further attempt.","parameters":[{"schema":{"type":"string","minLength":36,"maxLength":36},"in":"path","name":"id","required":true}],"security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"404":{"description":"Unknown or foreign delivery","content":{"application/json":{"schema":{"description":"Unknown or foreign delivery","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["delivery_not_found"]}},"required":["error"]}}}}}}},"/v1/domain/rotate-webhook-secret":{"post":{"tags":["webhooks"],"description":"Rotate the webhook signing secret (whsec_, shown once). During the 1-hour grace, deliveries carry two v1 signatures — verify against either.","security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}}}}},"/v1/storage/locations":{"get":{"tags":["storage"],"description":"Platform storage locations this instance offers. Pick one by id with backend \"s3_platform\" — the operator owns the bucket and the credentials, you choose the region.","security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox sessions cannot manage storage","content":{"application/json":{"schema":{"description":"Sandbox sessions cannot manage storage","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}}}}},"/v1/domain/storage":{"get":{"tags":["storage"],"description":"Where this domain stores NEW videos. Credentials are never returned — only the access key’s last 4. last_error and last_verified_at tell you whether the backend is currently working.","security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox sessions cannot manage storage","content":{"application/json":{"schema":{"description":"Sandbox sessions cannot manage storage","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}}}},"put":{"tags":["storage"],"description":"Choose where NEW videos are stored. Verified before it is saved — on failure nothing changes. Existing videos are never moved: they keep playing from wherever they were written (their backend is immutable). Changing bucket/endpoint/region creates a new profile version; supplying a new key for the same bucket rotates in place. Note key_prefix applies to OUTPUTS only — raw uploads are keyed by the tus store at the bucket root.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["backend"],"properties":{"backend":{"type":"string","enum":["local","s3_platform","s3_tenant"]},"location_id":{"type":"string","maxLength":64},"endpoint":{"type":"string","pattern":"^https?://","maxLength":255},"region":{"type":"string","maxLength":64},"bucket":{"type":"string","minLength":1,"maxLength":255},"key_prefix":{"type":"string","maxLength":128},"force_path_style":{"type":"boolean"},"access_key_id":{"type":"string","minLength":1,"maxLength":128},"secret_access_key":{"type":"string","minLength":1,"maxLength":256},"public_base_url":{"type":["string","null"],"pattern":"^https://","maxLength":500}}}}}},"security":[{"domainKey":[]}],"responses":{"400":{"description":"Bad shape, or the round trip failed","content":{"application/json":{"schema":{"description":"Bad shape, or the round trip failed","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["validation_error","storage_verification_failed"]}},"required":["error"]}}}},"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox sessions cannot manage storage","content":{"application/json":{"schema":{"description":"Sandbox sessions cannot manage storage","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}},"503":{"description":"Backend unreachable","content":{"application/json":{"schema":{"description":"Backend unreachable","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["storage_unavailable"]}},"required":["error"]}}}}}},"delete":{"tags":["storage"],"description":"Revert NEW uploads to this instance’s local disk. Videos already in your bucket stay there and keep playing; nothing is copied or deleted.","security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox sessions cannot manage storage","content":{"application/json":{"schema":{"description":"Sandbox sessions cannot manage storage","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}}}}},"/v1/domain/storage/verify":{"post":{"tags":["storage"],"description":"Dry-run a storage configuration: writes, reads, fetches a presigned URL and deletes a probe object, then reports what would happen. Persists nothing. Use this before PUT.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["backend"],"properties":{"backend":{"type":"string","enum":["local","s3_platform","s3_tenant"]},"location_id":{"type":"string","maxLength":64},"endpoint":{"type":"string","pattern":"^https?://","maxLength":255},"region":{"type":"string","maxLength":64},"bucket":{"type":"string","minLength":1,"maxLength":255},"key_prefix":{"type":"string","maxLength":128},"force_path_style":{"type":"boolean"},"access_key_id":{"type":"string","minLength":1,"maxLength":128},"secret_access_key":{"type":"string","minLength":1,"maxLength":256},"public_base_url":{"type":["string","null"],"pattern":"^https://","maxLength":500}}}}}},"security":[{"domainKey":[]}],"responses":{"400":{"description":"Bad shape, or the round trip failed","content":{"application/json":{"schema":{"description":"Bad shape, or the round trip failed","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["validation_error","storage_verification_failed"]}},"required":["error"]}}}},"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}},"403":{"description":"Sandbox sessions cannot manage storage","content":{"application/json":{"schema":{"description":"Sandbox sessions cannot manage storage","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["sandbox_not_allowed"]}},"required":["error"]}}}},"503":{"description":"Backend unreachable","content":{"application/json":{"schema":{"description":"Backend unreachable","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["storage_unavailable"]}},"required":["error"]}}}}}}},"/v1/usage":{"get":{"tags":["usage"],"description":"Today's usage (UTC): uploaded bytes, encoded seconds, absolute storage gauge, configured limits, and remaining headroom. Apps can pre-check before initiating uploads.","security":[{"domainKey":[]}],"responses":{"401":{"description":"Bad API key","content":{"application/json":{"schema":{"description":"Bad API key","type":"object","additionalProperties":true,"properties":{"error":{"type":"string","enum":["unauthorized"]}},"required":["error"]}}}}}}},"/v1/sandbox/session":{"post":{"tags":["tenancy"],"description":"Mint a 24h sandbox session key — zero-signup trial (3/day per IP). The key is a standard ek_ credential scoped to the shared sandbox domain: tiny quotas, watermarked output, 24h purge.","responses":{"200":{"description":"Default Response"}}}}},"tags":[{"name":"tenancy","description":"Your domain: settings, config, API key (ADR-02)"},{"name":"videos","description":"Video lifecycle: initiate, status, listing, cancel, delete (ADR-03/09)"},{"name":"upload","description":"Resumable tus upload protocol (ADR-03); see the upload guide"},{"name":"webhooks","description":"Outbound webhook deliveries: listing, replay, secret rotation (ADR-08)"},{"name":"usage","description":"Quotas and usage counters (ADR-09)"},{"name":"storage","description":"Where your videos are stored: local, a platform location, or your own S3 bucket (ADR-17)"},{"name":"playback","description":"Private videos: per-end-user playback keys and revocation (ADR-21)"},{"name":"outros","description":"Branded tail cards appended to web.mp4, the Open Graph artifact (ADR-22)"},{"name":"meta","description":"Health and service metadata"}]}