Bolrach Stream API reference

Generated from the deployed API contract on 2026-09-08. 38 operations.

Use https://api.bolrach.com as your base URL. Keep bearer API keys on your server. Your application must authenticate users and check asset and upload ownership before forwarding requests. Browser video bytes go directly to signed R2 URLs.

Start with the logo branding guide for position, opacity, image validation and creator ownership. Referenced schemas are in the downloadable OpenAPI document.

Jump to an endpoint
GET/v1/stream/health

Service health

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/docs

Human-readable documentation

Served publicly at https://docs.bolrach.com/stream.

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/openapi.json

This OpenAPI document

Public and unauthenticated: a spec you need a key to read is a spec nobody evaluates you on.

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/health/live

Liveness probe

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/watermarks

Upload a reusable tenant-owned PNG or JPEG logo

Request body

{
  "required": true,
  "content": {
    "application/json": {
      "schema": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Logo name"
          },
          "imageBase64": {
            "type": "string",
            "description": "Base64 PNG or JPEG, at most 1 MiB decoded"
          }
        },
        "required": [
          "imageBase64"
        ]
      }
    }
  }
}

Responses

{
  "201": {
    "description": "Success",
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "description": "Watermark ID used when creating an upload"
            },
            "name": {
              "type": "string",
              "description": "Logo name"
            },
            "bytes": {
              "type": "integer",
              "description": "Decoded file size"
            }
          }
        }
      }
    }
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/watermarks

List logos owned by this organisation

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/watermarks/{id}/image

Get a five-minute preview URL for an organisation-owned logo

Parameters

[
  {
    "name": "id",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/assets/{assetId}/source

Get a tenant-owned original for provenance or archival inspection

Returns a 60-second signed R2 URL for a ready asset. Keep it server-side; never expose it as a public playback link. Deleted assets are unavailable.

Parameters

[
  {
    "name": "assetId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success",
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "url": {
              "type": "string",
              "description": "Short-lived original download URL"
            },
            "bytes": {
              "type": "integer",
              "description": "Original size in bytes"
            },
            "expiresInSeconds": {
              "type": "integer",
              "description": "Expiry in seconds",
              "example": 60
            }
          }
        }
      }
    }
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/uploads

Start a resumable upload

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. Send `idempotencyKey` and a retry returns the SAME session instead of orphaning the first one and double-counting your quota.

Request body

{
  "required": true,
  "content": {
    "application/json": {
      "schema": {
        "type": "object",
        "properties": {
          "processingProfile": {
            "type": "string",
            "enum": [
              "standard_vod",
              "fast_social"
            ],
            "default": "standard_vod",
            "description": "fast_social caps landscape output at 1080p and portrait at 1920px height, retains lower bandwidth renditions, and skips synchronous VMAF telemetry. Original bytes are retained."
          },
          "watermark": {
            "type": "object",
            "properties": {
              "watermarkId": {
                "type": "string",
                "format": "uuid",
                "description": "ID returned by POST /watermarks; must belong to this organisation"
              },
              "position": {
                "type": "string",
                "enum": [
                  "tl",
                  "tr",
                  "bl",
                  "br"
                ],
                "default": "br",
                "description": "Top-left, top-right, bottom-left or bottom-right"
              },
              "opacity": {
                "type": "number",
                "minimum": 0.05,
                "maximum": 0.9,
                "default": 0.35
              }
            },
            "required": [
              "watermarkId"
            ]
          },
          "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/uploads/{uploadId}

Upload status and progress

Parameters

[
  {
    "name": "uploadId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
DELETE/v1/stream/uploads/{uploadId}

Abort an upload and release its quota

Parameters

[
  {
    "name": "uploadId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/uploads/{uploadId}/parts/sign

Get signed URLs for parts

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.

Parameters

[
  {
    "name": "uploadId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Request body

{
  "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/uploads/{uploadId}/complete

Finish the upload

Verifies every part arrived, then starts probing and encoding. Returns an error naming the missing parts rather than completing a broken object.

Parameters

[
  {
    "name": "uploadId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "201": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/uploads/{uploadId}/pause

Pause an upload

Parameters

[
  {
    "name": "uploadId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "201": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/uploads/{uploadId}/resume

Resume a paused upload

Parameters

[
  {
    "name": "uploadId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "201": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/assets

List your assets

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.

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/assets/{assetId}

Read one asset

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. While 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. Use `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.

Parameters

[
  {
    "name": "assetId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
PATCH/v1/stream/assets/{assetId}

Change who can watch an asset

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. Narrowing 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.

Parameters

[
  {
    "name": "assetId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Request body

{
  "required": true,
  "content": {
    "application/json": {
      "schema": {
        "type": "object",
        "properties": {
          "visibility": {
            "type": "string",
            "description": "private, signed, unlisted or public",
            "example": "signed"
          }
        },
        "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/assets/{assetId}/captions

Request captions for a published asset

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. The 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. Asking twice for the same asset and language does not queue the work twice.

Parameters

[
  {
    "name": "assetId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Request body

{
  "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/assets/{assetId}/playback-sessions

Mint a viewer playback token

Returns a short-lived signed token plus ready-to-use HLS and embed URLs. The 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.

Parameters

[
  {
    "name": "assetId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Request body

{
  "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
          },
          "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
DELETE/v1/stream/assets/playback-sessions/{sessionId}

Revoke a playback session immediately

Parameters

[
  {
    "name": "sessionId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/live/channels

Create a live channel

Returns ingest URLs and stream keys **once**. Only hashes are stored, so a lost key must be rotated rather than recovered. ⚠ The RTMP port is **1936**, not the default 1935. Use the URL exactly as returned. Live is currently PASSTHROUGH: viewers receive your source quality with no adaptive ladder.

Request body

{
  "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/live/channels

List live channels

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/live/channels/{channelId}

Channel status

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.

Parameters

[
  {
    "name": "channelId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/live/channels/{channelId}/keys/rotate

Rotate a stream key

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.

Parameters

[
  {
    "name": "channelId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Request body

{
  "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/live/channels/{channelId}/playback-sessions

Mint a viewer token for a live channel

Same controls as an on-demand session. A live token is refused on an on-demand path and vice versa.

Parameters

[
  {
    "name": "channelId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "201": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/rooms

Create an interactive room

A real-time room where participants publish and subscribe to each other. Issue a token per participant with `/tokens`.

Request body

{
  "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/rooms

List rooms

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/rooms/{roomId}/tokens

Issue a participant join token

Mint one per person. `role` is enforced, not advisory: a `viewer` token cannot publish media.

Parameters

[
  {
    "name": "roomId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Request body

{
  "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/rooms/{roomId}/participants

Who is in the room

Reconciled against the media server, which is the authority — not against our own bookkeeping.

Parameters

[
  {
    "name": "roomId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/rooms/{roomId}/broadcast

Broadcast a room to a live channel

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.

Parameters

[
  {
    "name": "roomId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Request body

{
  "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
DELETE/v1/stream/rooms/{roomId}

End a room and disconnect everyone

Parameters

[
  {
    "name": "roomId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/domains

Add a custom playback domain

Serve media from your own hostname. Returns the DNS record to create; the domain is not activated until control is proven.

Request body

{
  "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 plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/domains

List custom domains

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
GET/v1/stream/domains/{domainId}

Domain status

Parameters

[
  {
    "name": "domainId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
DELETE/v1/stream/domains/{domainId}

Remove a custom domain

Parameters

[
  {
    "name": "domainId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "200": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/domains/{domainId}/verify

Check verification and issue a certificate

Parameters

[
  {
    "name": "domainId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "string"
    }
  }
]

Responses

{
  "201": {
    "description": "Success"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}
POST/v1/stream/collect

Player QoE beacon

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.

Responses

{
  "204": {
    "description": "Accepted without a response body"
  },
  "400": {
    "description": "Invalid request or plan limit exceeded"
  },
  "401": {
    "description": "Missing or invalid API key"
  },
  "403": {
    "description": "Not permitted for this tenant"
  },
  "404": {
    "description": "Not found"
  },
  "409": {
    "description": "Conflicting state or concurrency limit"
  },
  "429": {
    "description": "Rate limited"
  }
}