{"openapi":"3.1.0","info":{"title":"Bolrach Stream","version":"1.0.0","summary":"Video infrastructure: upload, encode, deliver, live, and interactive rooms.","description":"Every endpoint is under `/v1/stream`. Authenticate with `Authorization: Bearer <api key>`.\n\nUploads go DIRECTLY to storage using signed URLs, so this API never sees your bytes and upload size is bounded by your plan rather than a request timeout.","contact":{"name":"Bolrach Stream","url":"https://docs.bolrach.dev/stream"}},"servers":[{"url":"https://api.bolrach.com","description":"Production"},{"url":"https://api.bolrach.stream","description":"Alias — /v1/* is rewritten to /v1/stream/*"}],"tags":[{"name":"Uploads","description":"Get media in"},{"name":"Assets","description":"Inspect what you have uploaded"},{"name":"Playback","description":"Let viewers watch it"},{"name":"Live","description":"Broadcast in real time"},{"name":"Rooms","description":"Interactive multi-party sessions"},{"name":"Custom domains","description":"Serve from your own hostname"},{"name":"Analytics","description":"Quality-of-experience telemetry"},{"name":"Health","description":"Service status"}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Your Bolrach Stream API key"}}},"security":[{"apiKey":[]}],"paths":{"/v1/stream/health":{"get":{"summary":"Service health","tags":["Health"],"security":[],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/docs":{"get":{"summary":"Human-readable documentation","description":"Served publicly at https://docs.bolrach.dev/stream.","tags":["Health"],"security":[],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/openapi.json":{"get":{"summary":"This OpenAPI document","description":"Public and unauthenticated: a spec you need a key to read is a spec nobody evaluates you on.","tags":["Health"],"security":[],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/health/live":{"get":{"summary":"Liveness probe","tags":["Health"],"security":[],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/uploads":{"post":{"summary":"Start a resumable upload","description":"Creates an upload session and an asset. The file is sent DIRECTLY to storage with the signed part URLs from `/parts/sign` — it never passes through this API, so upload size is limited by your plan rather than by any request timeout.\n\nSend `idempotencyKey` and a retry returns the SAME session instead of orphaning the first one and double-counting your quota.","tags":["Uploads"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"filename":{"type":"string","description":"Original file name","example":"lecture-01.mp4"},"bytes":{"type":"integer","description":"Exact size in bytes. Determines the part size and part count.","example":734003200},"mimeType":{"type":"string","description":"Source MIME type","example":"video/mp4"},"title":{"type":"string","description":"Human title for the asset","example":"Lecture 1 — Introduction"},"externalRef":{"type":"string","description":"Your own identifier, echoed back on the asset"},"idempotencyKey":{"type":"string","description":"Retry-safe key. The same key always returns the same upload."}},"required":["bytes"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"uploadId":{"type":"string","description":"Use this for every subsequent call"},"assetId":{"type":"string","description":"The asset this upload will become"},"partSizeBytes":{"type":"integer","description":"Split the file into parts of EXACTLY this size, except the last"},"partsExpected":{"type":"integer","description":"How many parts to send"},"recommendedConcurrency":{"type":"integer","description":"Parts to upload in parallel"},"expiresAt":{"type":"string","description":"Session expiry (ISO 8601)"}}}}}},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/uploads/{uploadId}":{"get":{"summary":"Upload status and progress","tags":["Uploads"],"parameters":[{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}},"delete":{"summary":"Abort an upload and release its quota","tags":["Uploads"],"parameters":[{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/uploads/{uploadId}/parts/sign":{"post":{"summary":"Get signed URLs for parts","description":"Returns short-lived PUT URLs. Sign only the parts you are about to send; re-signing a part is safe and is how a resumed upload continues.","tags":["Uploads"],"parameters":[{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"partNumbers":{"type":"array","items":{"type":"integer"},"description":"1-based part numbers","example":[1,2,3]}},"required":["partNumbers"]}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/uploads/{uploadId}/complete":{"post":{"summary":"Finish the upload","description":"Verifies every part arrived, then starts probing and encoding. Returns an error naming the missing parts rather than completing a broken object.","tags":["Uploads"],"parameters":[{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/uploads/{uploadId}/pause":{"post":{"summary":"Pause an upload","tags":["Uploads"],"parameters":[{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/uploads/{uploadId}/resume":{"post":{"summary":"Resume a paused upload","tags":["Uploads"],"parameters":[{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/watermarks":{"get":{"summary":"List tenant watermark images","description":"Requires watermarks:read. Returns up to 50 owned watermark records, newest first.","tags":["Uploads"],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}},"post":{"summary":"Register a watermark image","description":"Requires watermarks:write. Accepts a PNG or JPEG with decoded size from 1 byte to 1 MiB. The returned ID can be referenced when creating an upload.","tags":["Uploads"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Optional display name"},"imageBase64":{"type":"string","description":"PNG or JPEG as base64 or a data URI"}},"required":["imageBase64"]}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/collect":{"options":{"summary":"Telemetry CORS preflight","tags":["Analytics"],"security":[],"responses":{"204":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}},"post":{"summary":"Player QoE beacon","description":"Called by the player, not by your server. Authenticated by the playback token in the body — the only credential safe to put in a page.","tags":["Analytics"],"security":[],"responses":{"204":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/assets":{"get":{"summary":"List your assets","description":"Newest first. Filter with `?status=ready`. Upload without read is not a usable API — this exists because a real integration had no way to ask whether a video was ready.","tags":["Assets"],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/assets/{assetId}":{"get":{"summary":"Read one asset","description":"Carries `status`, `progress_percent`, `current_stage` and, on failure, a `failure_summary` you can show a user. While still uploading it also returns `upload_id`, so an interrupted client can finish the upload it started.\n\nWhile processing it also returns `encoding` — whether anything is happening to THIS video yet — plus `queuePosition`, `stageLabel` and `estimatedReadyAt`. Show the customer the difference between encoding and waiting: a queue is not progress. `estimatedReadyAt` is null when there is not enough history to estimate honestly, and `etaBasis` states what it was based on.\n\nUse `stage` for anything shown to a user: it is plain English, where `current_stage` is our internal enum. `progress_percent` is reported as 0 while `encoding` is false — it tracks pipeline stages rather than encode completion, and showing 40% for a video that has not started is a bar that lies.","tags":["Assets"],"parameters":[{"name":"assetId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}},"patch":{"summary":"Change who can watch an asset","description":"Sets `visibility` to one of `private`, `signed`, `unlisted` or `public`. Anything else is refused rather than ignored, so a typo cannot silently leave a video open.\n\nNarrowing visibility also removes the tokenless poster copy in the same request. That copy only exists while an asset is openly visible, and leaving it behind would mean the permission was gone but the object that granted it was not. The response carries the new `visibility` and the resulting `poster` state.","tags":["Assets"],"parameters":[{"name":"assetId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"visibility":{"type":"string","description":"private, signed, unlisted or public","example":"signed"},"operationId":{"type":"string","description":"UUID required together with expectedPolicyVersion for managed assets"},"expectedPolicyVersion":{"type":"string","description":"Exact current generation as a lossless decimal string"}},"required":["visibility"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Asset id"},"visibility":{"type":"string","description":"The visibility now in force"}}}}}},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/assets/{assetId}/policy":{"get":{"summary":"Read the current asset policy generation","description":"Returns the lossless decimal policy version, operation UUID, managed flag, current status and deletion_pending. Requires assets:read.","tags":["Assets"],"parameters":[{"name":"assetId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/assets/{assetId}/deletion":{"post":{"summary":"Request durable primary storage deletion","description":"Requires assets:write and the committed managed private policy operation and version. Repeating the same request returns the same deletion operation. Playback closes immediately; physical cleanup waits for retention, outstanding write grants and legal holds. A certificate acknowledges enumerated primary objects only. Archives remain retained.","tags":["Assets"],"parameters":[{"name":"assetId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"operationId":{"type":"string","description":"UUID of the committed private policy operation"},"expectedPolicyVersion":{"type":"string","description":"Its exact policy version as a decimal string"},"reason":{"type":"string","description":"Optional reason, at most 1000 characters"}},"required":["operationId","expectedPolicyVersion"]}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}},"get":{"summary":"Read durable deletion progress","description":"Requires assets:read. Returns deletionId, operationId, state, purgeAfter, physicalStarted, needsAttention, a stable error code, and the primary-scope certificate once confirmed. State can be requested, inventory, purging, retry, held, purged, cancelled or failed. Pending or failed cleanup is never an erasure acknowledgment.","tags":["Assets"],"parameters":[{"name":"assetId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/assets/{assetId}/deletion/cancel":{"post":{"summary":"Cancel deletion before physical cleanup starts","description":"Requires assets:write. Cancellation preserves private visibility and keeps prior viewer sessions revoked. It cannot reverse a staff-origin deletion or takedown, physical deletion already started, or a confirmed erasure.","tags":["Assets"],"parameters":[{"name":"assetId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/assets/{assetId}/captions":{"post":{"summary":"Request captions for a published asset","description":"Queues transcription and returns immediately — captions are not ready when this responds. The job is scheduled below encoding, because accessibility arriving minutes late beats publication arriving late.\n\nThe asset must be `ready`; captioning something still processing is refused with `asset_not_ready`. **English only** — the installed model produces English, so any other language is refused with `language_unsupported` rather than returning English text stored under a French label.\n\nAsking twice for the same asset and language does not queue the work twice.","tags":["Assets"],"parameters":[{"name":"assetId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"language":{"type":"string","description":"Only `en` is supported today","example":"en"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"assetId":{"type":"string","description":"Asset id"},"language":{"type":"string","description":"Language that was queued"},"requestId":{"type":"string","description":"Scheduler request id"},"state":{"type":"string","description":"Queue state at the moment of the response"},"captionPath":{"type":"string","description":"Where the .vtt will appear once the job completes","example":"captions/en.vtt"}}}}}},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/assets/{assetId}/playback-sessions":{"post":{"summary":"Mint a viewer playback token","description":"Returns a short-lived signed token plus ready-to-use HLS and embed URLs.\n\nThe token is **bound to the hostname** you pass as `host`: a token minted for one host is refused on another, which is what stops one custom-domain tenant replaying another's token. Mint per viewer, never share one token between viewers.","tags":["Playback"],"parameters":[{"name":"assetId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"host":{"type":"string","description":"Hostname the token will be presented on","example":"media.bolrach.video"},"expiresInSeconds":{"type":"integer","description":"60 to 86400. Default 3600.","example":3600},"publicOnly":{"type":"boolean","description":"Require public or unlisted visibility at issuance and every delivery request"},"requiredPolicyVersion":{"type":"string","description":"Optional exact current policy generation as a decimal string"},"viewerId":{"type":"string","description":"Your identifier for this viewer, for analytics and concurrency limits"},"allowedCountries":{"type":"array","items":{"type":"string"},"description":"ISO 3166-1 alpha-2. Empty means everywhere."},"blockedCountries":{"type":"array","items":{"type":"string"},"description":"ISO 3166-1 alpha-2."},"maxConcurrent":{"type":"integer","description":"Maximum simultaneous SESSIONS this viewerId may hold. Minting beyond it returns 409 viewer_concurrency_reached. Requires viewerId. Note this caps sessions, not players: one issued token opened by several people at once counts as one session."}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string","description":"Revoke with DELETE /assets/playback-sessions/{sessionId}"},"token":{"type":"string","description":"The signed viewer token"},"hlsUrl":{"type":"string","description":"Direct HLS manifest URL, token included"},"embedUrl":{"type":"string","description":"Drop this in an iframe"},"expiresAt":{"type":"string","description":"ISO 8601"},"durationSeconds":{"type":"integer","description":"Asset duration"}}}}}},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/assets/playback-sessions/{sessionId}":{"delete":{"summary":"Revoke a playback session immediately","tags":["Playback"],"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/live/channels":{"post":{"summary":"Create a live channel","description":"Returns ingest URLs and stream keys **once**. Only hashes are stored, so a lost key must be rotated rather than recovered.\n\n⚠ The RTMP port is **1936**, not the default 1935. Use the URL exactly as returned.\n\nLive is currently PASSTHROUGH: viewers receive your source quality with no adaptive ladder.","tags":["Live"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Channel name","example":"Main Stage"},"slug":{"type":"string","description":"URL-safe identifier. Derived from the name if omitted."},"record":{"type":"boolean","description":"Record broadcasts into ordinary on-demand assets. Default true."},"dvrSeconds":{"type":"integer","description":"DVR window in seconds","example":7200}},"required":["name"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"channelId":{"type":"string","description":"Channel id"},"ingest":{"type":"object","properties":{"rtmp":{"type":"object","properties":{"url":{"type":"string","description":"rtmp://ingest.bolrach.video:1936/live"},"streamKey":{"type":"string","description":"Shown once"}}},"backup":{"type":"object","properties":{"url":{"type":"string","description":"Same URL"},"streamKey":{"type":"string","description":"Failover key — the supported way to take over"}}},"srt":{"type":"string","description":"SRT ingest URL"}}},"playback":{"type":"object","properties":{"hls":{"type":"string","description":"LL-HLS playback URL"}}}}}}}},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}},"get":{"summary":"List live channels","tags":["Live"],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/live/channels/{channelId}":{"get":{"summary":"Channel status","description":"State is `idle`, `connecting` or `live`. `live` means a viewer can actually watch right now — it is asserted against the packager output, not merely against a publisher having connected.","tags":["Live"],"parameters":[{"name":"channelId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/live/channels/{channelId}/keys/rotate":{"post":{"summary":"Rotate a stream key","description":"Returns the new key and the full ingest URL, so nobody has to remember the non-default port while re-pointing an encoder under pressure.","tags":["Live"],"parameters":[{"name":"channelId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"which":{"type":"string","enum":["primary","backup"],"description":"Default primary"}}}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/live/channels/{channelId}/playback-sessions":{"post":{"summary":"Mint a viewer token for a live channel","description":"Same controls as an on-demand session. A live token is refused on an on-demand path and vice versa.","tags":["Live"],"parameters":[{"name":"channelId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/rooms":{"post":{"summary":"Create an interactive room","description":"A real-time room where participants publish and subscribe to each other. Issue a token per participant with `/tokens`.","tags":["Rooms"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Room name","example":"Weekly standup"},"maxParticipants":{"type":"integer","description":"Capped by your plan"},"record":{"type":"boolean","description":"Record the room"},"emptyTimeoutSeconds":{"type":"integer","description":"Close the room this long after the last participant leaves","example":300}},"required":["name"]}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}},"get":{"summary":"List rooms","tags":["Rooms"],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/rooms/{roomId}/tokens":{"post":{"summary":"Issue a participant join token","description":"Mint one per person. `role` is enforced, not advisory: a `viewer` token cannot publish media.","tags":["Rooms"],"parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"identity":{"type":"string","description":"Your identifier for this participant"},"displayName":{"type":"string","description":"Shown to other participants"},"role":{"type":"string","enum":["host","participant","viewer"],"description":"viewer cannot publish"},"ttlSeconds":{"type":"integer","description":"60 to 43200. Default 3600."}}}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/rooms/{roomId}/participants":{"get":{"summary":"Who is in the room","description":"Reconciled against the media server, which is the authority — not against our own bookkeeping.","tags":["Rooms"],"parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/rooms/{roomId}/broadcast":{"post":{"summary":"Broadcast a room to a live channel","description":"Composites the room and publishes it to one of your live channels through the ordinary live ingest, so it gets the same recording, metering and playback as any other broadcast.","tags":["Rooms"],"parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"channelId":{"type":"string","description":"Target live channel"},"streamKey":{"type":"string","description":"That channel's stream key"}},"required":["channelId","streamKey"]}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/rooms/{roomId}":{"delete":{"summary":"End a room and disconnect everyone","tags":["Rooms"],"parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/domains":{"post":{"summary":"Add a custom playback domain","description":"Serve media from your own hostname. Returns the DNS record to create; the domain is not activated until control is proven.","tags":["Custom domains"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hostname":{"type":"string","description":"e.g. video.example.com"}},"required":["hostname"]}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}},"get":{"summary":"List custom domains","tags":["Custom domains"],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/domains/{domainId}":{"get":{"summary":"Domain status","tags":["Custom domains"],"parameters":[{"name":"domainId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}},"delete":{"summary":"Remove a custom domain","tags":["Custom domains"],"parameters":[{"name":"domainId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}},"/v1/stream/domains/{domainId}/verify":{"post":{"summary":"Check verification and issue a certificate","tags":["Custom domains"],"parameters":[{"name":"domainId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid request or active legal hold"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Not permitted for this tenant"},"404":{"description":"Not found"},"409":{"description":"Policy conflict or irreversible lifecycle state"},"429":{"description":"Rate limited"},"503":{"description":"Dependency unavailable; retain and retry the same operation"}}}}},"x-route-count":{"documented":41}}