# API reference

> Every endpoint, parameter and field of the JustOnAir API, generated from openapi.json.

All endpoints live at `https://api.justonair.com`. Authenticate with `Authorization: Bearer <API key>`. Requests and responses are JSON. Times are ISO 8601 UTC; money is a decimal string in USD.

This page is generated from [openapi.json](/openapi.json). Import that file into Postman, an SDK generator or an agent framework to get the same thing as code.

## Streams

Create, read, change and end streams.

### Create a stream

```http
POST /v1/streams
```

Reserves ingest capacity for 30 minutes and returns everything needed to go live: `rtmp_url` + `stream_key`, `whip_url`, `playback_url` and `embed_url`. The secrets are returned only here. Send an `Idempotency-Key` so a retry never creates a second stream.

| Parameter | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Any unique string, e.g. a UUID. A retry with the same key and body returns the first response (header `Idempotent-Replayed: true`) for 24 h. |

**Body** (optional)

| Field | Type | Description |
|---|---|---|
| `name` | string \| null | Up to 100 characters. Blank means no name. |
| `profile` | "abr_basic" \| "passthrough" | Default `abr_basic`. |
| `max_resolution` | 480 \| 720 \| 1080 | The tallest video you will send. Default 720. 1080 needs a trusted account. Sending more than you declare ends the stream after a 20 s warning. |
| `record` | boolean | Make an MP4 after the stream ends. Default true. |
| `recording_retention_days` | integer \| null | Days to keep the MP4, 1–3650. Omit for the account default (30). Null: keep until deleted. |
| `metadata` | object | Your own JSON, up to 2 KB. |
| `player` | PlayerSettings |  |


**Responses**

| Status | Body | Meaning |
|---|---|---|
| 201 | `CreatedStream` | Created. Store `stream_key` and `whip_url` now. |
| 400 | `Error` | Invalid request (`invalid_request`). |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 402 | `Error` | Credit below the 1.00 USD minimum (`insufficient_credit`). |
| 403 | `Error` | Account pending approval or suspended (`account_pending`, `tenant_suspended`). |
| 422 | `Error` | This Idempotency-Key was used with a different body (`idempotency_key_reused`). |
| 429 | `Error` | An account limit: `limit_pending_streams`, `limit_creation_rate`, `limit_ingest_hours`, `limit_resolution`. |
| 503 | `Error` | No ingest capacity right now (`no_capacity`). Retry shortly. |

### List streams

```http
GET /v1/streams
```

Newest first.

| Parameter | In | Type | Description |
|---|---|---|---|
| `limit` | query | integer |  |
| `starting_after` | query | string | Id of the last stream on the previous page. |
| `status` | query | "pending" \| "live" \| "ended" \| "expired" \| "cancelled" |  |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `StreamList` | A page of streams. |
| 400 | `Error` | Invalid request (`invalid_request`). |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 403 | `Error` | Account pending approval or suspended (`account_pending`, `tenant_suspended`). |

### Get a stream

```http
GET /v1/streams/{id}
```

Status, what is arriving (`ingest`), the recording, and `cost` so far. Poll it to follow a stream; every 3–5 s is fine.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Stream id. |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `Stream` | The stream. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 403 | `Error` | Account pending approval or suspended (`account_pending`, `tenant_suspended`). |
| 404 | `Error` | No such stream in your account (`not_found`). |

### Update a stream

```http
PATCH /v1/streams/{id}
```

Rename it, replace its metadata, or change player settings. Works in any status.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Stream id. |

**Body**. Only these fields can change, in any status. Null clears a field; `metadata` is replaced whole.

| Field | Type | Description |
|---|---|---|
| `name` | string \| null | Up to 100 characters. |
| `metadata` | object \| null |  |
| `player` | PlayerSettings |  |


**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `Stream` | The stream. |
| 400 | `Error` | Invalid request (`invalid_request`). |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 403 | `Error` | Account pending approval or suspended (`account_pending`, `tenant_suspended`). |
| 404 | `Error` | No such stream in your account (`not_found`). |

### End or cancel a stream

```http
DELETE /v1/streams/{id}
```

Live: ends it and disconnects the publisher within seconds. Pending: cancels it and releases the slot. Already finished: returns it unchanged.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Stream id. |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `Stream` | The stream, now `ended` or `cancelled`. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 403 | `Error` | Account pending approval or suspended (`account_pending`, `tenant_suspended`). |
| 404 | `Error` | No such stream in your account (`not_found`). |

### Replace a lost stream key

```http
POST /v1/streams/{id}/stream-key
```

Only while pending. The old key and WHIP URL stop working at once.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Stream id. |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `CreatedStream` | The stream with new `stream_key` and `whip_url`. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such stream in your account (`not_found`). |
| 409 | `Error` | Live or finished (`stream_not_pending`). |

### Viewers per minute

```http
GET /v1/streams/{id}/viewers
```

The viewer curve: live estimates from the hosted player, and exact counts from the CDN logs about an hour later.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Stream id. |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `ViewerSeries` | Per-minute counts from both sources. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such stream in your account (`not_found`). |

## Playback

Signed URLs for your own player.

### Sign a playback URL

```http
POST /v1/streams/{id}/playback-token
```

A fresh signed HLS URL, for example a short one per viewer minted by your backend. Refused once the stream is finished.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Stream id. |

**Body** (optional)

| Field | Type | Description |
|---|---|---|
| `expires_in` | integer | Seconds. Default: until the stream could no longer be live, plus an hour. |


**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `PlaybackToken` | Signed URL. |
| 400 | `Error` | Invalid request (`invalid_request`). |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such stream in your account (`not_found`). |
| 409 | `Error` | The stream has ended (`stream_ended`). There is no replay. |

## Recordings

The MP4 made after a stream ends.

### Delete the recording

```http
DELETE /v1/streams/{id}/recording
```

Deletes the MP4 now, or marks it to be deleted as soon as it exists.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Stream id. |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `Stream` | The stream; `recording.status` is `deleted` or `deleting`. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such stream in your account (`not_found`). |

### Get a download link

```http
POST /v1/streams/{id}/recording/download
```



| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Stream id. |

**Body** (optional)

| Field | Type | Description |
|---|---|---|
| `expires_in` | integer |  |


**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `RecordingDownload` | A signed link to the MP4. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such stream in your account (`not_found`). |
| 409 | `Error` | Not ready (`recording_not_ready`, with `details.status`). |

## Account

Balance and usage.

### Balance and usage

```http
GET /v1/usage
```



| Parameter | In | Type | Description |
|---|---|---|---|
| `days` | query | integer |  |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `Usage` | Balance, rates and usage per UTC day. |
| 400 | `Error` | Invalid request (`invalid_request`). |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |

## Webhooks

Endpoints that get stream and recording events, and their delivery log.

### List webhook endpoints

```http
GET /v1/webhooks
```

Without secrets. Also returns `event_types`, every type an endpoint can subscribe to.

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `WebhookEndpointList` | Your endpoints. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |

### Add a webhook endpoint

```http
POST /v1/webhooks
```

Up to 5 per account. The answer includes `secret`; see [Webhooks](/webhooks) for verifying signatures.

**Body**

| Field | Type | Description |
|---|---|---|
| `url` | string | https URL on the public internet, up to 2000 characters. |
| `events` | array of "stream.live" \| "stream.ended" \| "stream.expired" \| "stream.cancelled" \| "recording.ready" \| "recording.failed" \| "recording.deleted" | Leave out for all events, including ones added later. |
| `description` | string | Up to 200 characters. |


**Responses**

| Status | Body | Meaning |
|---|---|---|
| 201 | `WebhookEndpoint` | The endpoint, with its secret. |
| 400 | `Error` | Invalid request (`invalid_request`). |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 429 | `Error` | 5 endpoints already (`limit_webhook_endpoints`). |

### Get a webhook endpoint

```http
GET /v1/webhooks/{id}
```

With its secret.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Endpoint id (`whk_…`). |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `WebhookEndpoint` | The endpoint. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such endpoint (`not_found`). |

### Change a webhook endpoint

```http
PATCH /v1/webhooks/{id}
```



| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Endpoint id (`whk_…`). |

**Body**

| Field | Type | Description |
|---|---|---|
| `url` | string |  |
| `events` | array of "stream.live" \| "stream.ended" \| "stream.expired" \| "stream.cancelled" \| "recording.ready" \| "recording.failed" \| "recording.deleted" |  |
| `description` | string \| null |  |
| `enabled` | boolean | False stops sending; queued deliveries fail. |


**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `WebhookEndpoint` | The endpoint. |
| 400 | `Error` | Invalid request (`invalid_request`). |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such endpoint (`not_found`). |

### Delete a webhook endpoint

```http
DELETE /v1/webhooks/{id}
```

Its delivery log goes with it.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Endpoint id (`whk_…`). |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 204 |  | Deleted. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such endpoint (`not_found`). |

### Rotate the signing secret

```http
POST /v1/webhooks/{id}/secret
```

The old secret keeps signing alongside the new one for 24 hours.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Endpoint id (`whk_…`). |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `WebhookEndpoint` | The endpoint with the new secret. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such endpoint (`not_found`). |

### Send a test event

```http
POST /v1/webhooks/{id}/test
```

Queues a `webhook.test`; it is sent within a couple of seconds.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Endpoint id (`whk_…`). |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 202 | `WebhookDelivery` | The queued delivery. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such endpoint (`not_found`). |

### Delivery log

```http
GET /v1/webhooks/{id}/deliveries
```

Newest first, kept 30 days.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Endpoint id (`whk_…`). |
| `limit` | query | integer |  |
| `status` | query | "pending" \| "succeeded" \| "failed" |  |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `WebhookDeliveryList` | Deliveries, newest first. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No such endpoint (`not_found`). |

### Send a delivery again

```http
POST /v1/webhooks/{id}/deliveries/{delivery_id}/retry
```

A failed delivery gets one more attempt now; a pending one is brought forward.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Endpoint id (`whk_…`). |
| `delivery_id` | path | string | `dlv_…` |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 202 | `WebhookDelivery` | The delivery. |
| 401 | `Error` | Missing or invalid API key (`unauthorized`). |
| 404 | `Error` | No pending or failed delivery with that id (`not_found`). |

## Embed

The hosted player’s public read.

### Public stream status (hosted player)

```http
GET /v1/embed/{id}
```

No key needed; any web page may call it. Cached for a few seconds; the same read is served, cached at the CDN edge, at `https://play.joacdn.com/api/embed/{id}`. Rate limited per IP on unknown ids (60/min).

No API key needed.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Stream id. |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 200 | `Embed` | Status and, while live, a signed URL. |
| 404 | `Error` | Unknown stream. |
| 429 | `Error` | Too many requests (`rate_limited`). |

### Report a viewer (sampled)

```http
POST /v1/embed/{id}/beat
```

Sent by the hosted player, or by your own player to be counted live. Draw `u` once per page load; report every `beacon.interval_seconds` while `u` is below `beacon.rate` from the embed read. No body, so `navigator.sendBeacon(url)` works from any page.

No API key needed.

| Parameter | In | Type | Description |
|---|---|---|---|
| `id` | path | string | Stream id. |
| `s` | query | string | Random id for this page load. |
| `st` | query | "w" \| "p" \| "z" \| "b" | Waiting, playing, paused or buffering. |
| `u` | query | number | The random draw, 0 ≤ u < 1. |

**Responses**

| Status | Body | Meaning |
|---|---|---|
| 204 |  | Counted (or the stream is over and there is nothing to count). |
| 400 | `Error` | Invalid request (`invalid_request`). |
| 404 | `Error` | Unknown stream. |
| 429 | `Error` | Too many requests (`rate_limited`). |

## Objects

### Stream

| Field | Type | Description |
|---|---|---|
| `id` | string | Stream id. Also the folder of its video on the CDN. |
| `object` | string | Always `stream`. |
| `name` | string \| null | Your label, up to 100 characters. Shown on the hosted player unless `player.show_name` is false. |
| `status` | "pending" \| "live" \| "ended" \| "expired" \| "cancelled" | `pending` (waiting for the publisher, up to 30 min), `live`, `ended`, `expired` (nothing connected in time) or `cancelled` (deleted before going live). |
| `profile` | "abr_basic" \| "passthrough" | `abr_basic`: several qualities for viewers. `passthrough`: exactly what you send. |
| `record` | boolean | Whether an MP4 is made after the stream ends. |
| `recording` | Recording |  |
| `node_ready` | boolean | The ingest server is up. False for about a minute when a new server has to start. |
| `rtmp_url` | string \| null | RTMP server URL for OBS or ffmpeg. Use with `stream_key`. |
| `playback_url` | string \| null | Signed HLS URL for your own player. Null once the stream is finished (there is no replay). Re-signed on every read. |
| `playback_url_expires_at` | string \| null | When `playback_url` stops working. |
| `embed_url` | string | The hosted player page. Put it in an iframe or share it as a link. |
| `renditions` | array of string | Qualities viewers will get, e.g. `["source", "480p"]`. |
| `max_ingest_resolution` | integer | The tallest video you declared you will send: 480, 720 or 1080. |
| `max_duration_seconds` | integer | The stream ends after this long live. 4 h for new accounts, 24 h for trusted ones. |
| `metadata` | object \| null | Your own JSON, up to 2 KB. Never shown to viewers. |
| `player` | PlayerSettings |  |
| `reservation_expires_at` | string \| null | While pending: when the reserved slot is released and the stream expires. |
| `reservation_expires_in_seconds` | integer \| null | The same as a countdown, computed by the server. |
| `created_at` | string |  |
| `went_live_at` | string \| null |  |
| `ended_at` | string \| null |  |
| `end_reason` | string \| null | Why it ended: `deleted`, `disconnected`, `duration_limit`, `reservation_ttl`, `limit_bitrate`, `limit_resolution`, `insufficient_credit`, `simulcast_not_supported`, `provision_failed`. |
| `ingest` | Ingest |  |
| `cost` | Cost |  |
| `viewers` | Viewers |  |
| `viewers_now` | integer \| null | Watching on the hosted player right now (the same estimate as `viewers.now.watching`). Null unless the stream is pending or live and someone is on the player. Also in the list, so one call shows every live audience. |


### CreatedStream

A stream plus its secrets. Returned once, by the create call (and by replacing the key). Store them; they are never shown again.

| Field | Type | Description |
|---|---|---|
| `id` | string | Stream id. Also the folder of its video on the CDN. |
| `object` | string | Always `stream`. |
| `name` | string \| null | Your label, up to 100 characters. Shown on the hosted player unless `player.show_name` is false. |
| `status` | "pending" \| "live" \| "ended" \| "expired" \| "cancelled" | `pending` (waiting for the publisher, up to 30 min), `live`, `ended`, `expired` (nothing connected in time) or `cancelled` (deleted before going live). |
| `profile` | "abr_basic" \| "passthrough" | `abr_basic`: several qualities for viewers. `passthrough`: exactly what you send. |
| `record` | boolean | Whether an MP4 is made after the stream ends. |
| `recording` | Recording |  |
| `node_ready` | boolean | The ingest server is up. False for about a minute when a new server has to start. |
| `rtmp_url` | string \| null | RTMP server URL for OBS or ffmpeg. Use with `stream_key`. |
| `playback_url` | string \| null | Signed HLS URL for your own player. Null once the stream is finished (there is no replay). Re-signed on every read. |
| `playback_url_expires_at` | string \| null | When `playback_url` stops working. |
| `embed_url` | string | The hosted player page. Put it in an iframe or share it as a link. |
| `renditions` | array of string | Qualities viewers will get, e.g. `["source", "480p"]`. |
| `max_ingest_resolution` | integer | The tallest video you declared you will send: 480, 720 or 1080. |
| `max_duration_seconds` | integer | The stream ends after this long live. 4 h for new accounts, 24 h for trusted ones. |
| `metadata` | object \| null | Your own JSON, up to 2 KB. Never shown to viewers. |
| `player` | PlayerSettings |  |
| `reservation_expires_at` | string \| null | While pending: when the reserved slot is released and the stream expires. |
| `reservation_expires_in_seconds` | integer \| null | The same as a countdown, computed by the server. |
| `created_at` | string |  |
| `went_live_at` | string \| null |  |
| `ended_at` | string \| null |  |
| `end_reason` | string \| null | Why it ended: `deleted`, `disconnected`, `duration_limit`, `reservation_ttl`, `limit_bitrate`, `limit_resolution`, `insufficient_credit`, `simulcast_not_supported`, `provision_failed`. |
| `ingest` | Ingest |  |
| `cost` | Cost |  |
| `viewers` | Viewers |  |
| `viewers_now` | integer \| null | Watching on the hosted player right now (the same estimate as `viewers.now.watching`). Null unless the stream is pending or live and someone is on the player. Also in the list, so one call shows every live audience. |
| `stream_key` | string | RTMP stream key, `{id}?key={secret}`. OBS: paste as Stream Key with `rtmp_url` as Server. One-shot: works for this stream only. |
| `whip_url` | string | WHIP endpoint for browsers and WebRTC encoders. Contains the secret. |


### Ingest

What the ingest node last measured arriving. Null until the first measurement.

| Field | Type | Description |
|---|---|---|
| `width` | integer \| null | Frame width in pixels. |
| `height` | integer \| null | Frame height in pixels. |
| `kbps` | integer \| null | Bitrate of everything received (video, audio, overhead), averaged over about 30 s. |
| `observed_at` | string | When this was measured. |
| `limit_warning` | string \| null | Set while a limit is broken: `limit_bitrate` or `limit_resolution`. Still broken 20 s later, the stream ends with that `end_reason`. |
| `keyframe_interval_seconds` | number \| null | Average seconds between the publisher’s keyframes. Null for browser (WHIP) publishers. |
| `keyframe_warning` | boolean | True above 2.5 s. Set your encoder to a 2 s keyframe interval. A warning only; never ends a stream. |


### Recording

The recording: one MP4 of the top quality, made after the stream ends. Null while pending.

| Field | Type | Description |
|---|---|---|
| `status` | "recording" \| "processing" \| "ready" \| "deleting" \| "deleted" \| "failed" \| "not_recorded" \| "none" \| "legacy" | `recording` while live, then `processing`, `ready`, `deleting`, `deleted`, `failed`, `not_recorded` (record: false) or `none` (never went live). |
| `bytes` | integer \| null | MP4 size. |
| `duration_seconds` | number \| null | MP4 length. |
| `retention_days` | integer \| null | Days kept after it is ready. Null: until you delete it. |
| `ready_at` | string \| null | When the MP4 became downloadable. |
| `delete_at` | string \| null | When it will be deleted: the retention date, or 7 days after the balance reached zero, whichever is earlier. |
| `delete_reason` | string \| null | `retention` or `zero_balance`. |
| `deleted_at` | string \| null | When it was deleted. |


### Cost

What this stream has cost so far at current rates. Only on `GET /v1/streams/{id}`. Amounts are decimal strings.

| Field | Type | Description |
|---|---|---|
| `total_usd` | string | Everything below. |
| `delivery_bytes` | integer | Bytes delivered to viewers, counted up to `delivery_counted_until`. |
| `delivery_usd` | string | Delivery cost. |
| `delivery_counted_until` | string \| null | CDN logs are read an hour at a time, so delivery trails reality by up to about 80 minutes. |
| `ingest_seconds` | integer | Seconds of publishing. |
| `ingest_usd` | string | Encoding (abr_basic) or passthrough cost. |
| `storage_gb_hours` | number | Recording storage used. |
| `storage_usd` | string | Recording storage cost. |


### Viewers

Viewer counts. While the stream runs, `now` is estimated from the hosted player: exact up to about 400 viewers, within about 5% above. After the fact, the CDN logs give the exact count for every player (hosted, your own, apps), about an hour later; `peak` and `watch_minutes` use them once they exist. Only on `GET /v1/streams/{id}`.

| Field | Type | Description |
|---|---|---|
| `now` | object \| null | Live estimate from the hosted player. Null when nobody is on the player or the stream is over. |
| `peak` | integer \| null | Most viewers in any one minute. |
| `peak_at` | string \| null | The start of that minute. |
| `watch_minutes` | integer \| null | Total minutes watched, all viewers together. |
| `source` | "cdn" \| "player" \| null | Where `peak` and `watch_minutes` come from: `cdn` (every player, exact) or `player` (hosted player estimate, until the CDN count arrives). |
| `cdn_counted_until` | string \| null | CDN logs are read up to this time. |


### ViewerSeries

| Field | Type | Description |
|---|---|---|
| `object` | string | Always `viewer_series`. |
| `stream_id` | string | Stream id. Also the folder of its video on the CDN. |
| `player` | array of object | Per minute, from the hosted player (live estimate). |
| `cdn` | array of object | Per minute, from the CDN logs: average viewers watching at once, every player included. |
| `countries` | array of object | Where the watch time came from, from the CDN logs (every player), largest first, up to 30. Grows hour by hour while the stream runs. |


### PlayerSettings

| Field | Type | Description |
|---|---|---|
| `show_name` | boolean | Show the stream name to viewers on the hosted player. Default true. False keeps the name out of the public embed read entirely. |


### StreamList

| Field | Type | Description |
|---|---|---|
| `object` | string | Always `list`. |
| `data` | array of Stream |  |
| `has_more` | boolean | More streams after the last one. Pass its id as `starting_after`. |


### PlaybackToken

| Field | Type | Description |
|---|---|---|
| `object` | string | Always `playback_token`. |
| `stream_id` | string | Stream id. Also the folder of its video on the CDN. |
| `playback_url` | string | Signed HLS URL. |
| `expires_at` | string \| null |  |


### RecordingDownload

| Field | Type | Description |
|---|---|---|
| `object` | string | Always `recording_download`. |
| `stream_id` | string | Stream id. Also the folder of its video on the CDN. |
| `url` | string | Signed link to the MP4 on the CDN. Downloading it counts as delivery. |
| `bytes` | integer | MP4 size. |
| `expires_at` | string |  |


### Embed

What the hosted player needs. Public: no key, any origin may read it.

| Field | Type | Description |
|---|---|---|
| `object` | string | Always `embed`. |
| `id` | string | Stream id. Also the folder of its video on the CDN. |
| `status` | "pending" \| "live" \| "ended" \| "unavailable" | `pending`, `live`, `ended`, or `unavailable` (expired or cancelled). |
| `title` | string \| null | The stream name when it is to be shown, otherwise null. |
| `playback_url` | string \| null | While live: a signed HLS URL, the same for every viewer. |
| `playback_url_expires_at` | string \| null |  |
| `went_live_at` | string \| null |  |
| `ended_at` | string \| null |  |
| `poll_after_seconds` | integer \| null | Ask again after this many seconds (5 pending, 60 live). Null: stop asking. |
| `beacon` | object \| null | Viewer counting: a player whose random number is below `rate` reports every `interval_seconds` (see Count viewers from your own player). Null once the stream is over. |


### Usage

| Field | Type | Description |
|---|---|---|
| `object` | string | Always `usage`. |
| `balance_usd` | string | Credit minus everything charged so far. |
| `available_usd` | string | Balance minus used-but-not-yet-charged. Live streams end when this reaches zero. |
| `balance_depleted_at` | string \| null | Set while the balance is at or below zero. |
| `recordings_delete_at` | string \| null | 7 days after the balance reached zero: when recordings will be deleted unless you top up. |
| `rates` | object |  |
| `days` | array of object |  |


### WebhookEndpoint

| Field | Type | Description |
|---|---|---|
| `object` | string | Always `webhook_endpoint`. |
| `id` | string | `whk_…` |
| `url` | string | Where events are POSTed. https only. |
| `description` | string \| null | Your own note. |
| `events` | array of "stream.live" \| "stream.ended" \| "stream.expired" \| "stream.cancelled" \| "recording.ready" \| "recording.failed" \| "recording.deleted" | Event types sent to this endpoint. |
| `all_events` | boolean | True when subscribed to everything, including event types added later. |
| `enabled` | boolean | False: nothing is sent. |
| `created_at` | string |  |
| `secret` | string | `whsec_…`, for verifying signatures. Only on create, `GET /v1/webhooks/{id}` and rotation. |


### WebhookDelivery

| Field | Type | Description |
|---|---|---|
| `object` | string | Always `webhook_delivery`. |
| `id` | string | `dlv_…` |
| `event_id` | string | `evt_…`, the `webhook-id` header. |
| `event_type` | string |  |
| `stream_id` | string \| null |  |
| `status` | "pending" \| "succeeded" \| "failed" |  |
| `attempts` | integer |  |
| `max_attempts` | integer |  |
| `next_attempt_at` | string \| null |  |
| `last_attempt_at` | string \| null |  |
| `last_status_code` | integer \| null | What your server answered. |
| `last_error` | string \| null | Why the last attempt failed (`answered 500`, a timeout, a DNS error, …). |
| `last_duration_ms` | integer \| null |  |
| `delivered_at` | string \| null |  |
| `created_at` | string |  |
| `payload` | WebhookEvent |  |


### WebhookEvent

The body of every webhook. Headers: `webhook-id`, `webhook-timestamp`, `webhook-signature` (Standard Webhooks).

| Field | Type | Description |
|---|---|---|
| `type` | "stream.live" \| "stream.ended" \| "stream.expired" \| "stream.cancelled" \| "recording.ready" \| "recording.failed" \| "recording.deleted" \| "webhook.test" |  |
| `timestamp` | string | When it happened. |
| `data` | object | For stream and recording events, the stream at that moment. For `webhook.test`, the endpoint. |


### Error

| Field | Type | Description |
|---|---|---|
| `error` | object |  |

