reference

Rate limits

/v1 is rate-limited per credential with a token-bucket limiter. Each API key gets its own independent bucket, so one key hitting its limit never throttles another key — including other keys on your own account. Requests made with a user access token share one bucket per user, however many tokens that user holds.

A request that fails authentication is rejected before it reaches a bucket, so it never spends a token.

How the bucket works

  • The bucket holds up to a burst of tokens and refills at a steady sustained rate (tokens per second).
  • Each request spends one token. If a token is available, the request proceeds.
  • If the bucket is empty, the request is rejected with 429 rate_limited and a Retry-After header (in seconds) telling you how long until a token refills.

This means you can burst up to the bucket size after an idle period, then settle into the sustained rate. Today the bucket holds 60 tokens and refills at 10 per second. Treat those numbers as subject to change and drive your client off the Retry-After header, not a hardcoded figure.

Credential-minting endpoints

Endpoints that hand out a live credential have a second, much tighter bucket on top of the general one: 5 requests, refilling at 5 per minute, per credential. They are:

  • POST /v1/studios/{id}/sources/{sourceId}/ingest
  • POST /v1/studios/{id}/sources/{sourceId}/ingest/rotate
  • POST /v1/studios/{id}/sources/{sourceId}/camera/prepare
  • POST / DELETE /v1/studios/{id}/contribute
  • POST /v1/studios/{id}/monitor

Past it you get the same 429 rate_limited + Retry-After, with a message saying the credential requests are limited. Fetch a credential once and reuse it rather than asking again per attempt.

The public playback token endpoint

POST /v1/playback/{playbackId}/token takes no credential, so it can't have a per-credential bucket. It has two buckets of its own instead, and a request must fit within both:

  • per viewer address: 20 requests in a burst, refilling at 10 per minute;
  • per playback id: 200 requests in a burst, refilling at 50 per second, shared by the playback's whole audience.

Its 429 carries Retry-After (never less than 1) and repeats the number in the body as error.retryAfterSeconds. Successful responses carry a retryPolicy with the backoff bounds a player should use. See Embed a player on your site.

Handling a 429

// HTTP 429, header: Retry-After: 3
{
  "error": {
    "code": "rate_limited",
    "message": "rate limit exceeded; retry after the Retry-After interval",
    "requestId": "req_…"
  }
}
  1. Read the Retry-After response header (seconds).
  2. Wait at least that long.
  3. Retry the request.
  4. If you're making many calls, add jitter and exponential backoff so a fleet of clients doesn't retry in lockstep.
resp = call()
if resp.status == 429:
  wait(seconds = resp.header("Retry-After") + jitter)
  retry with exponential backoff

Staying under the limit

  • Cache reads. Studio/source/destination lists change slowly; don't poll them in a tight loop.
  • Page with a large limit. Fetch up to 200 items per request instead of many small pages (see Pagination).
  • Use one key per integration. Because buckets are per-key, splitting distinct workloads across distinct keys keeps one busy job from starving another — and makes it easy to revoke just one.
  • Back off on 429, don't hammer. A client that ignores Retry-After and retries immediately just stays rate-limited.

Limits and tiers will be documented here as they firm up. For now, build against the 429 + Retry-After contract and you'll be forward-compatible.