# Streams

A stream is one broadcast. Book it (now or for later), point an encoder
at its ingest address, start it, and end it. A stream is billed per
minute from when it starts to when it ends, with a 15-minute minimum.

Base URL: `https://api.cardinalstreaming.com`. Authenticate with `Authorization: Bearer <your API key>`.

## List streams

`GET /v1/streams`

Your streams, newest booked first. Page with `limit` and `before`:
pass the previous page's `next_before` as `before` until it is absent.
The ingest key is never in a list.

### Query parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `state` | string | no | Only streams in this state. One of `"scheduled"`, `"starting"`, `"live"`, `"ended"`, `"failed"`. |
| `customer_ref` | string | no | Only this customer's streams. |
| `recording_state` | string | no | Only streams whose recording is in this state (as you would see it, so a ready recording past its 30 days is `expired`). One of `"none"`, `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `limit` | integer | no | Page size, 1-200. Default 50; more than 200 is treated as 200. |
| `before` | string | no | The previous page's `next_before`. Streams booked before this time. |

### Examples

cURL:

```bash
curl "https://api.cardinalstreaming.com/v1/streams?state=live&customer_ref=grace-chapel" \
  -H "Authorization: Bearer $CARDINAL_API_KEY"
```

Python:

```python
import os

import requests

response = requests.get(
    "https://api.cardinalstreaming.com/v1/streams",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    params={
        "state": "live",
        "customer_ref": "grace-chapel",
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/streams?state=live&customer_ref=grace-chapel', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/streams?state=live&customer_ref=grace-chapel', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.cardinalstreaming.com/v1/streams', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'query' => [
        'state' => 'live',
        'customer_ref' => 'grace-chapel',
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/streams?state=live&customer_ref=grace-chapel")
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "https://api.cardinalstreaming.com/v1/streams?state=live&customer_ref=grace-chapel", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(data))
}
```

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/streams?state=live&customer_ref=grace-chapel"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .method("GET", HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

```csharp
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("GET"), "https://api.cardinalstreaming.com/v1/streams?state=live&customer_ref=grace-chapel");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 200

A page of streams.

| Name | Type | Required | Description |
|---|---|---|---|
| `streams` | array of object | yes |  |
| `streams[].playback_id` | string | yes | The stream's public id. Unguessable. |
| `streams[].customer_ref` | string | yes | Your identifier for a customer. 1-128 letters, digits, `.`, `_` or `-`; not `.` or `..`. |
| `streams[].title` | string | yes |  |
| `streams[].tier` | string | yes | `standard_1080p` ($5 per stream-hour) or `pro_4k` ($20 per stream-hour). One of `"standard_1080p"`, `"pro_4k"`. |
| `streams[].state` | string | yes |  One of `"scheduled"`, `"starting"`, `"live"`, `"ended"`, `"failed"`. |
| `streams[].scheduled_start_at` | string (date-time) | no | When a booking starts itself. |
| `streams[].started_at` | string (date-time) | no |  |
| `streams[].ended_at` | string (date-time) | no |  |
| `streams[].unit_rate_usd_per_hour` | number | no | The stream-hour rate, fixed when it started. |
| `streams[].restream_unit_rate_usd_per_hour` | number | no | The destination-hour rate, fixed when it started. |
| `streams[].leg_count` | integer | yes | How many independent encodings the stream runs. |
| `streams[].legs_ready` | integer | yes | How many are encoding now. |
| `streams[].legs` | array of object | no | Each encoding's state, while the stream is not over. |
| `streams[].legs[].rank` | integer | yes |  |
| `streams[].legs[].state` | string | yes |  One of `"pending"`, `"ready"`, `"unhealthy"`. |
| `streams[].legs[].ready_since` | string (date-time) | no |  |
| `streams[].recording` | object | yes |  |
| `streams[].recording.state` | string | yes |  One of `"none"`, `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `streams[].recording.bytes` | integer (int64) | no |  |
| `streams[].recording.duration_seconds` | number | no |  |
| `streams[].recording.ready_at` | string (date-time) | no |  |
| `streams[].ingest` | object | no | Where an encoder sends the stream: RTMPS (RTMP inside TLS) on port 443. Ingest is encrypted only; plain RTMP is not accepted. The srt_ fields appear only where SRT is offered, and SRT is encrypted too: one passphrase per stream, shown with the key. |
| `streams[].ingest.key` | string | no | The ingest key. Only when it was just created or rotated. |
| `streams[].ingest.server_url` | string | no | The RTMPS server, for an encoder's Server field. |
| `streams[].ingest.stream_key` | string | no | The stream key, for the encoder's Stream Key field. Only with `key`. |
| `streams[].ingest.srt_url` | string | no | Only where SRT is offered. The whole SRT address for one-field encoders: stream ID, passphrase and AES-256. Only with `key`. |
| `streams[].ingest.srt_server` | string | no | Only where SRT is offered. The SRT server, srt://host:port. Not a secret. |
| `streams[].ingest.srt_stream_id` | string | no | Only where SRT is offered. publish:live/<playback_id>: names the broadcast, carries no key. Not a secret. |
| `streams[].ingest.srt_passphrase` | string | no | Only where SRT is offered. The stream's SRT passphrase: the encoder encrypts with it, and anything unencrypted or on another passphrase is refused. Only with `key`; a new key brings a new one. |
| `streams[].ingest.endpoints` | array of string | yes | Every RTMPS address the stream accepts. |
| `streams[].ingest.fanout_required` | boolean | yes | Whether the encoder must push to every endpoint for full redundancy. |
| `streams[].encoder_connected` | boolean | yes | Whether video is arriving from an encoder now. |
| `streams[].viewer_access` | object | yes |  |
| `streams[].viewer_access.visibility` | string | yes |  One of `"public"`, `"private"`. |
| `streams[].viewer_access.has_access_code` | boolean | yes |  |
| `streams[].viewer_access.interactions` | object | yes | What a stream's live room offers. |
| `streams[].viewer_access.interactions.chat` | boolean | yes |  |
| `streams[].viewer_access.interactions.reactions` | boolean | yes |  |
| `streams[].viewer_access.interactions.highlights` | boolean | yes | Gift highlights from your giving system. |
| `streams[].viewer_access.interactions.reaction_floats` | boolean | yes | Whether reactions float up the player. |
| `streams[].viewer_access.interactions.highlight_display` | boolean | yes | Whether highlights are shown in the player. |
| `streams[].viewer_access.access_code` | string | no | Present only in the response that set or generated the code. Never readable again. |
| `streams[].end_reason` | string or null | yes | Why the stream ended. Null until it has. One of `"customer_ended"`, `"no_video"`, `"max_duration"`, `"failed"`, `"not_started"`. |
| `streams[].start_failure` | string or null | yes | Why a booking could not start. Null unless it could not. One of `"cap_exceeded"`, `"already_on_air"`, `"no_rate"`, `"missed_window"`, `"org_inactive"`, `"payment_required"`. |
| `streams[].captions` | object | no |  |
| `streams[].captions.spoken_language` | string | yes | `auto`, or the language spoken: one of `ar de en es fr it ja ko pl pt ru tl uk vi zh`. |
| `streams[].captions.translation_languages` | array of string | yes | Reserved for translated captions. Empty. |
| `streams[].captions.translation_mode` | string | yes |  One of `"on_demand"`, `"always"`. |
| `streams[].captions.profanity_masking` | boolean | yes |  |
| `streams[].captions.sung_lyrics` | string | yes | Whether sung lyrics are captioned or shown as a music symbol. One of `"music-symbol"`, `"caption"`. |
| `streams[].captions.vocabulary` | array of string | yes | Up to 100 names and terms the captions should spell right. |
| `streams[].captions.retired_source_languages` | array of string | no |  |
| `next_before` | string (date-time) | no | Present when there may be another page. Pass it as `before`. |

```json
{
  "streams": [
    {
      "playback_id": "1846bdac2d8c0ce0e846d4a3817012c9",
      "customer_ref": "grace-chapel",
      "title": "Sunday Service",
      "tier": "standard_1080p",
      "state": "live",
      "started_at": "2026-10-18T15:00:02Z",
      "unit_rate_usd_per_hour": 5,
      "leg_count": 3,
      "legs_ready": 3,
      "legs": [
        {
          "rank": 0,
          "state": "ready",
          "ready_since": "2026-10-18T15:00:09Z"
        },
        {
          "rank": 1,
          "state": "ready",
          "ready_since": "2026-10-18T15:00:11Z"
        },
        {
          "rank": 2,
          "state": "ready",
          "ready_since": "2026-10-18T15:00:10Z"
        }
      ],
      "recording": {
        "state": "none"
      },
      "encoder_connected": true,
      "viewer_access": {
        "visibility": "public",
        "has_access_code": false,
        "interactions": {
          "chat": true,
          "reactions": true,
          "highlights": true,
          "reaction_floats": true,
          "highlight_display": true
        }
      },
      "end_reason": null,
      "start_failure": null
    }
  ],
  "next_before": "2026-09-18T11:52:07.421Z"
}
```

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_state` | `state` is not one of the stream states. |
| 400 | `invalid_customer_ref` | `customer_ref` is not a valid customer reference. |
| 400 | `invalid_recording_state` | `recording_state` is not one of the recording states. |
| 400 | `invalid_limit` | `limit` is not a positive whole number. |
| 400 | `invalid_before` | `before` is not an RFC 3339 time. |

## Book a stream

`POST /v1/streams`

Books a broadcast, now or for later, and returns its ingest address.
**The ingest key is returned once, here, and never again** (rotate it
to get a new one).

A booking with `scheduled_start_at` starts itself at that time, and
**billing starts then whether or not your encoder is connected**. Leave
it out and call [start](#startStream) when you are ready. An encoder
can connect up to an hour before a booked start to check the picture;
nothing is billed or shown until the start.

Send an `Idempotency-Key` so a retried request returns the first
booking instead of making a second.

### Headers

| Name | Type | Required | Description |
|---|---|---|---|
| `Idempotency-Key` | string | no | Any unique string you choose per booking, up to 255 printable ASCII characters with no spaces (a UUID is ideal). A retry with the same key and the same body returns the original stream (`201`, without the ingest key); the same key with a different body is `422 idempotency_key_reused`; a longer key, or one with spaces or other characters, is `400 invalid_idempotency_key`. |

### Request body

| Name | Type | Required | Description |
|---|---|---|---|
| `customer_ref` | string | yes | Your identifier for a customer. 1-128 letters, digits, `.`, `_` or `-`; not `.` or `..`. |
| `title` | string | yes | Up to 200 characters. |
| `tier` | string | yes | `standard_1080p` ($5 per stream-hour) or `pro_4k` ($20 per stream-hour). One of `"standard_1080p"`, `"pro_4k"`. |
| `scheduled_start_at` | string (date-time) | no | Book it for later. Left out, start it with the start call. |
| `visibility` | string | no | Default `public`. One of `"public"`, `"private"`. |
| `access_code` | string | no | For a private stream: 6-32 letters, digits or hyphens. Left out, one is generated. |
| `interactions` | object | no | Switches to change; any left out stay as they are. |
| `interactions.chat` | boolean | no |  |
| `interactions.reactions` | boolean | no |  |
| `interactions.highlights` | boolean | no |  |
| `interactions.reaction_floats` | boolean | no |  |
| `interactions.highlight_display` | boolean | no |  |

### Examples

cURL:

```bash
curl -X POST "https://api.cardinalstreaming.com/v1/streams" \
  -H "Authorization: Bearer $CARDINAL_API_KEY" \
  -H "Idempotency-Key: 6f1c9a2e-booking-0001" \
  -H "Content-Type: application/json" \
  -d '{
  "customer_ref": "grace-chapel",
  "title": "Sunday Service",
  "tier": "standard_1080p",
  "scheduled_start_at": "2026-10-18T15:00:00Z"
}'
```

Python:

```python
import os

import requests

response = requests.post(
    "https://api.cardinalstreaming.com/v1/streams",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
        "Idempotency-Key": "6f1c9a2e-booking-0001",
    },
    json={
        "customer_ref": "grace-chapel",
        "title": "Sunday Service",
        "tier": "standard_1080p",
        "scheduled_start_at": "2026-10-18T15:00:00Z",
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/streams', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
    "Idempotency-Key": "6f1c9a2e-booking-0001",
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "customer_ref": "grace-chapel",
    "title": "Sunday Service",
    "tier": "standard_1080p",
    "scheduled_start_at": "2026-10-18T15:00:00Z"
  }),
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/streams', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
    "Idempotency-Key": "6f1c9a2e-booking-0001",
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "customer_ref": "grace-chapel",
    "title": "Sunday Service",
    "tier": "standard_1080p",
    "scheduled_start_at": "2026-10-18T15:00:00Z"
  }),
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('POST', 'https://api.cardinalstreaming.com/v1/streams', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
        'Idempotency-Key' => '6f1c9a2e-booking-0001',
    ],
    'json' => [
        'customer_ref' => 'grace-chapel',
        'title' => 'Sunday Service',
        'tier' => 'standard_1080p',
        'scheduled_start_at' => '2026-10-18T15:00:00Z',
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/streams")
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"
request["Idempotency-Key"] = "6f1c9a2e-booking-0001"
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
  "customer_ref" => "grace-chapel",
  "title" => "Sunday Service",
  "tier" => "standard_1080p",
  "scheduled_start_at" => "2026-10-18T15:00:00Z"
})

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	req, err := http.NewRequest("POST", "https://api.cardinalstreaming.com/v1/streams", strings.NewReader(`{
  "customer_ref": "grace-chapel",
  "title": "Sunday Service",
  "tier": "standard_1080p",
  "scheduled_start_at": "2026-10-18T15:00:00Z"
}`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))
	req.Header.Set("Idempotency-Key", "6f1c9a2e-booking-0001")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(data))
}
```

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/streams"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .header("Idempotency-Key", "6f1c9a2e-booking-0001")
            .header("Content-Type", "application/json")
            .method("POST", HttpRequest.BodyPublishers.ofString("{\"customer_ref\":\"grace-chapel\",\"title\":\"Sunday Service\",\"tier\":\"standard_1080p\",\"scheduled_start_at\":\"2026-10-18T15:00:00Z\"}"))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

```csharp
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("POST"), "https://api.cardinalstreaming.com/v1/streams");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));
request.Headers.Add("Idempotency-Key", "6f1c9a2e-booking-0001");
request.Content = new StringContent("{\"customer_ref\":\"grace-chapel\",\"title\":\"Sunday Service\",\"tier\":\"standard_1080p\",\"scheduled_start_at\":\"2026-10-18T15:00:00Z\"}", Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 201

The booked stream, with its ingest address and key.

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes | The stream's public id. Unguessable. |
| `customer_ref` | string | yes | Your identifier for a customer. 1-128 letters, digits, `.`, `_` or `-`; not `.` or `..`. |
| `title` | string | yes |  |
| `tier` | string | yes | `standard_1080p` ($5 per stream-hour) or `pro_4k` ($20 per stream-hour). One of `"standard_1080p"`, `"pro_4k"`. |
| `state` | string | yes |  One of `"scheduled"`, `"starting"`, `"live"`, `"ended"`, `"failed"`. |
| `scheduled_start_at` | string (date-time) | no | When a booking starts itself. |
| `started_at` | string (date-time) | no |  |
| `ended_at` | string (date-time) | no |  |
| `unit_rate_usd_per_hour` | number | no | The stream-hour rate, fixed when it started. |
| `restream_unit_rate_usd_per_hour` | number | no | The destination-hour rate, fixed when it started. |
| `leg_count` | integer | yes | How many independent encodings the stream runs. |
| `legs_ready` | integer | yes | How many are encoding now. |
| `legs` | array of object | no | Each encoding's state, while the stream is not over. |
| `legs[].rank` | integer | yes |  |
| `legs[].state` | string | yes |  One of `"pending"`, `"ready"`, `"unhealthy"`. |
| `legs[].ready_since` | string (date-time) | no |  |
| `recording` | object | yes |  |
| `recording.state` | string | yes |  One of `"none"`, `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `recording.bytes` | integer (int64) | no |  |
| `recording.duration_seconds` | number | no |  |
| `recording.ready_at` | string (date-time) | no |  |
| `ingest` | object | no | Where an encoder sends the stream: RTMPS (RTMP inside TLS) on port 443. Ingest is encrypted only; plain RTMP is not accepted. The srt_ fields appear only where SRT is offered, and SRT is encrypted too: one passphrase per stream, shown with the key. |
| `ingest.key` | string | no | The ingest key. Only when it was just created or rotated. |
| `ingest.server_url` | string | no | The RTMPS server, for an encoder's Server field. |
| `ingest.stream_key` | string | no | The stream key, for the encoder's Stream Key field. Only with `key`. |
| `ingest.srt_url` | string | no | Only where SRT is offered. The whole SRT address for one-field encoders: stream ID, passphrase and AES-256. Only with `key`. |
| `ingest.srt_server` | string | no | Only where SRT is offered. The SRT server, srt://host:port. Not a secret. |
| `ingest.srt_stream_id` | string | no | Only where SRT is offered. publish:live/<playback_id>: names the broadcast, carries no key. Not a secret. |
| `ingest.srt_passphrase` | string | no | Only where SRT is offered. The stream's SRT passphrase: the encoder encrypts with it, and anything unencrypted or on another passphrase is refused. Only with `key`; a new key brings a new one. |
| `ingest.endpoints` | array of string | yes | Every RTMPS address the stream accepts. |
| `ingest.fanout_required` | boolean | yes | Whether the encoder must push to every endpoint for full redundancy. |
| `encoder_connected` | boolean | yes | Whether video is arriving from an encoder now. |
| `viewer_access` | object | yes |  |
| `viewer_access.visibility` | string | yes |  One of `"public"`, `"private"`. |
| `viewer_access.has_access_code` | boolean | yes |  |
| `viewer_access.interactions` | object | yes | What a stream's live room offers. |
| `viewer_access.interactions.chat` | boolean | yes |  |
| `viewer_access.interactions.reactions` | boolean | yes |  |
| `viewer_access.interactions.highlights` | boolean | yes | Gift highlights from your giving system. |
| `viewer_access.interactions.reaction_floats` | boolean | yes | Whether reactions float up the player. |
| `viewer_access.interactions.highlight_display` | boolean | yes | Whether highlights are shown in the player. |
| `viewer_access.access_code` | string | no | Present only in the response that set or generated the code. Never readable again. |
| `end_reason` | string or null | yes | Why the stream ended. Null until it has. One of `"customer_ended"`, `"no_video"`, `"max_duration"`, `"failed"`, `"not_started"`. |
| `start_failure` | string or null | yes | Why a booking could not start. Null unless it could not. One of `"cap_exceeded"`, `"already_on_air"`, `"no_rate"`, `"missed_window"`, `"org_inactive"`, `"payment_required"`. |
| `captions` | object | no |  |
| `captions.spoken_language` | string | yes | `auto`, or the language spoken: one of `ar de en es fr it ja ko pl pt ru tl uk vi zh`. |
| `captions.translation_languages` | array of string | yes | Reserved for translated captions. Empty. |
| `captions.translation_mode` | string | yes |  One of `"on_demand"`, `"always"`. |
| `captions.profanity_masking` | boolean | yes |  |
| `captions.sung_lyrics` | string | yes | Whether sung lyrics are captioned or shown as a music symbol. One of `"music-symbol"`, `"caption"`. |
| `captions.vocabulary` | array of string | yes | Up to 100 names and terms the captions should spell right. |
| `captions.retired_source_languages` | array of string | no |  |

```json
{
  "playback_id": "1846bdac2d8c0ce0e846d4a3817012c9",
  "customer_ref": "grace-chapel",
  "title": "Sunday Service",
  "tier": "standard_1080p",
  "state": "scheduled",
  "scheduled_start_at": "2026-10-18T15:00:00Z",
  "leg_count": 3,
  "legs_ready": 0,
  "recording": {
    "state": "none"
  },
  "ingest": {
    "key": "strmi_Zk4q0V9yR1sT7uW3xY5zA2bC4dE6fG8hJ2kL5mN8pQr",
    "server_url": "rtmps://ingest.cardinalstreaming.com:443/live",
    "stream_key": "1846bdac2d8c0ce0e846d4a3817012c9?key=strmi_Zk4q0V9yR1sT7uW3xY5zA2bC4dE6fG8hJ2kL5mN8pQr",
    "endpoints": [
      "rtmps://ingest.cardinalstreaming.com:443/live/1846bdac2d8c0ce0e846d4a3817012c9"
    ],
    "fanout_required": false
  },
  "encoder_connected": false,
  "viewer_access": {
    "visibility": "public",
    "has_access_code": false,
    "interactions": {
      "chat": true,
      "reactions": true,
      "highlights": true,
      "reaction_floats": true,
      "highlight_display": true
    }
  },
  "end_reason": null,
  "start_failure": null
}
```

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_body` | The body is not a JSON object of the documented fields. |
| 400 | `invalid_tier` | `tier` is not `standard_1080p` or `pro_4k`. |
| 400 | `missing_field` | `title` is missing. |
| 400 | `invalid_customer_ref` | `customer_ref` is missing or not a valid customer reference. |
| 400 | `invalid_title` | `title` is longer than 200 characters. |
| 400 | `invalid_scheduled_start` | `scheduled_start_at` is more than 15 minutes in the past. |
| 400 | `invalid_interactions` | `interactions` is not an object of the documented switches. |
| 400 | `invalid_viewer_access` | `visibility` or `access_code` is not acceptable; `detail` says why. |
| 400 | `invalid_idempotency_key` | `Idempotency-Key` is longer than 255 characters, or has spaces or characters outside printable ASCII. |
| 402 | `card_required` | An organization that signed itself up has no card on file yet. Add one in the console (Billing). |
| 402 | `payment_required` | Streaming is paused: a bill is unpaid after its seven days. Pay it in the console (Billing); streaming resumes at once. |
| 409 | `already_active` | This customer already has a stream on air and your organization limits that. |
| 409 | `customer_archived` | The customer is archived. Unarchive it to book streams for it. |
| 422 | `idempotency_key_reused` | This `Idempotency-Key` was used for a different booking. |

## Read a stream

`GET /v1/streams/{playback_id}`

A stream's state, its legs (the three independent encodings) while it is
on air, whether an encoder is connected, and its recording's state. Poll
it every few seconds while live; it contains no credential.

### Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes | The stream's `playback_id`. |

### Examples

cURL:

```bash
curl "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9" \
  -H "Authorization: Bearer $CARDINAL_API_KEY"
```

Python:

```python
import os

import requests

response = requests.get(
    "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9")
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(data))
}
```

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .method("GET", HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

```csharp
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("GET"), "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 200

The stream.

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes | The stream's public id. Unguessable. |
| `customer_ref` | string | yes | Your identifier for a customer. 1-128 letters, digits, `.`, `_` or `-`; not `.` or `..`. |
| `title` | string | yes |  |
| `tier` | string | yes | `standard_1080p` ($5 per stream-hour) or `pro_4k` ($20 per stream-hour). One of `"standard_1080p"`, `"pro_4k"`. |
| `state` | string | yes |  One of `"scheduled"`, `"starting"`, `"live"`, `"ended"`, `"failed"`. |
| `scheduled_start_at` | string (date-time) | no | When a booking starts itself. |
| `started_at` | string (date-time) | no |  |
| `ended_at` | string (date-time) | no |  |
| `unit_rate_usd_per_hour` | number | no | The stream-hour rate, fixed when it started. |
| `restream_unit_rate_usd_per_hour` | number | no | The destination-hour rate, fixed when it started. |
| `leg_count` | integer | yes | How many independent encodings the stream runs. |
| `legs_ready` | integer | yes | How many are encoding now. |
| `legs` | array of object | no | Each encoding's state, while the stream is not over. |
| `legs[].rank` | integer | yes |  |
| `legs[].state` | string | yes |  One of `"pending"`, `"ready"`, `"unhealthy"`. |
| `legs[].ready_since` | string (date-time) | no |  |
| `recording` | object | yes |  |
| `recording.state` | string | yes |  One of `"none"`, `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `recording.bytes` | integer (int64) | no |  |
| `recording.duration_seconds` | number | no |  |
| `recording.ready_at` | string (date-time) | no |  |
| `ingest` | object | no | Where an encoder sends the stream: RTMPS (RTMP inside TLS) on port 443. Ingest is encrypted only; plain RTMP is not accepted. The srt_ fields appear only where SRT is offered, and SRT is encrypted too: one passphrase per stream, shown with the key. |
| `ingest.key` | string | no | The ingest key. Only when it was just created or rotated. |
| `ingest.server_url` | string | no | The RTMPS server, for an encoder's Server field. |
| `ingest.stream_key` | string | no | The stream key, for the encoder's Stream Key field. Only with `key`. |
| `ingest.srt_url` | string | no | Only where SRT is offered. The whole SRT address for one-field encoders: stream ID, passphrase and AES-256. Only with `key`. |
| `ingest.srt_server` | string | no | Only where SRT is offered. The SRT server, srt://host:port. Not a secret. |
| `ingest.srt_stream_id` | string | no | Only where SRT is offered. publish:live/<playback_id>: names the broadcast, carries no key. Not a secret. |
| `ingest.srt_passphrase` | string | no | Only where SRT is offered. The stream's SRT passphrase: the encoder encrypts with it, and anything unencrypted or on another passphrase is refused. Only with `key`; a new key brings a new one. |
| `ingest.endpoints` | array of string | yes | Every RTMPS address the stream accepts. |
| `ingest.fanout_required` | boolean | yes | Whether the encoder must push to every endpoint for full redundancy. |
| `encoder_connected` | boolean | yes | Whether video is arriving from an encoder now. |
| `viewer_access` | object | yes |  |
| `viewer_access.visibility` | string | yes |  One of `"public"`, `"private"`. |
| `viewer_access.has_access_code` | boolean | yes |  |
| `viewer_access.interactions` | object | yes | What a stream's live room offers. |
| `viewer_access.interactions.chat` | boolean | yes |  |
| `viewer_access.interactions.reactions` | boolean | yes |  |
| `viewer_access.interactions.highlights` | boolean | yes | Gift highlights from your giving system. |
| `viewer_access.interactions.reaction_floats` | boolean | yes | Whether reactions float up the player. |
| `viewer_access.interactions.highlight_display` | boolean | yes | Whether highlights are shown in the player. |
| `viewer_access.access_code` | string | no | Present only in the response that set or generated the code. Never readable again. |
| `end_reason` | string or null | yes | Why the stream ended. Null until it has. One of `"customer_ended"`, `"no_video"`, `"max_duration"`, `"failed"`, `"not_started"`. |
| `start_failure` | string or null | yes | Why a booking could not start. Null unless it could not. One of `"cap_exceeded"`, `"already_on_air"`, `"no_rate"`, `"missed_window"`, `"org_inactive"`, `"payment_required"`. |
| `captions` | object | no |  |
| `captions.spoken_language` | string | yes | `auto`, or the language spoken: one of `ar de en es fr it ja ko pl pt ru tl uk vi zh`. |
| `captions.translation_languages` | array of string | yes | Reserved for translated captions. Empty. |
| `captions.translation_mode` | string | yes |  One of `"on_demand"`, `"always"`. |
| `captions.profanity_masking` | boolean | yes |  |
| `captions.sung_lyrics` | string | yes | Whether sung lyrics are captioned or shown as a music symbol. One of `"music-symbol"`, `"caption"`. |
| `captions.vocabulary` | array of string | yes | Up to 100 names and terms the captions should spell right. |
| `captions.retired_source_languages` | array of string | no |  |

```json
{
  "playback_id": "1846bdac2d8c0ce0e846d4a3817012c9",
  "customer_ref": "grace-chapel",
  "title": "Sunday Service",
  "tier": "standard_1080p",
  "state": "live",
  "started_at": "2026-10-18T15:00:02Z",
  "unit_rate_usd_per_hour": 5,
  "leg_count": 3,
  "legs_ready": 3,
  "legs": [
    {
      "rank": 0,
      "state": "ready",
      "ready_since": "2026-10-18T15:00:09Z"
    },
    {
      "rank": 1,
      "state": "ready",
      "ready_since": "2026-10-18T15:00:11Z"
    },
    {
      "rank": 2,
      "state": "ready",
      "ready_since": "2026-10-18T15:00:10Z"
    }
  ],
  "recording": {
    "state": "none"
  },
  "encoder_connected": true,
  "viewer_access": {
    "visibility": "public",
    "has_access_code": false,
    "interactions": {
      "chat": true,
      "reactions": true,
      "highlights": true,
      "reaction_floats": true,
      "highlight_display": true
    }
  },
  "end_reason": null,
  "start_failure": null
}
```

### Errors

| Status | Code | Meaning |
|---|---|---|
| 404 | `not_found` | No such stream in your organization. |

## Change who may watch, and the room

`PATCH /v1/streams/{playback_id}`

Makes a stream public or private, sets or regenerates its access code,
switches chat, reactions and highlights, and (when captions are enabled)
changes its caption settings. Allowed in any state; a new code applies
to new viewers, not to URLs already handed out.

### Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes | The stream's `playback_id`. |

### Request body

| Name | Type | Required | Description |
|---|---|---|---|
| `visibility` | string | no |  One of `"public"`, `"private"`. |
| `access_code` | string | no | 6-32 letters, digits or hyphens. |
| `regenerate_access_code` | boolean | no | Generate a new access code. |
| `interactions` | object | no | Switches to change; any left out stay as they are. |
| `interactions.chat` | boolean | no |  |
| `interactions.reactions` | boolean | no |  |
| `interactions.highlights` | boolean | no |  |
| `interactions.reaction_floats` | boolean | no |  |
| `interactions.highlight_display` | boolean | no |  |
| `captions` | object | no |  |
| `captions.spoken_language` | string | no |  |
| `captions.translation_languages` | array of string | no |  |
| `captions.translation_mode` | string | no |  One of `"on_demand"`, `"always"`. |
| `captions.profanity_masking` | boolean | no |  |
| `captions.sung_lyrics` | string | no |  One of `"music-symbol"`, `"caption"`. |
| `captions.vocabulary` | array of string | no |  |

### Examples

cURL:

```bash
curl -X PATCH "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9" \
  -H "Authorization: Bearer $CARDINAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "visibility": "private",
  "regenerate_access_code": true,
  "interactions": {
    "chat": true,
    "reactions": true,
    "highlights": false
  }
}'
```

Python:

```python
import os

import requests

response = requests.patch(
    "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    json={
        "visibility": "private",
        "regenerate_access_code": True,
        "interactions": {
            "chat": True,
            "reactions": True,
            "highlights": False,
        },
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "visibility": "private",
    "regenerate_access_code": true,
    "interactions": {
      "chat": true,
      "reactions": true,
      "highlights": false
    }
  }),
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "visibility": "private",
    "regenerate_access_code": true,
    "interactions": {
      "chat": true,
      "reactions": true,
      "highlights": false
    }
  }),
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('PATCH', 'https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'json' => [
        'visibility' => 'private',
        'regenerate_access_code' => true,
        'interactions' => [
            'chat' => true,
            'reactions' => true,
            'highlights' => false,
        ],
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9")
request = Net::HTTP::Patch.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
  "visibility" => "private",
  "regenerate_access_code" => true,
  "interactions" => {
    "chat" => true,
    "reactions" => true,
    "highlights" => false
  }
})

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	req, err := http.NewRequest("PATCH", "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9", strings.NewReader(`{
  "visibility": "private",
  "regenerate_access_code": true,
  "interactions": {
    "chat": true,
    "reactions": true,
    "highlights": false
  }
}`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(data))
}
```

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .header("Content-Type", "application/json")
            .method("PATCH", HttpRequest.BodyPublishers.ofString("{\"visibility\":\"private\",\"regenerate_access_code\":true,\"interactions\":{\"chat\":true,\"reactions\":true,\"highlights\":false}}"))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

```csharp
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("PATCH"), "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));
request.Content = new StringContent("{\"visibility\":\"private\",\"regenerate_access_code\":true,\"interactions\":{\"chat\":true,\"reactions\":true,\"highlights\":false}}", Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 200

The stream as it now is. `viewer_access.access_code` is present only when this call set or generated one.

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes | The stream's public id. Unguessable. |
| `customer_ref` | string | yes | Your identifier for a customer. 1-128 letters, digits, `.`, `_` or `-`; not `.` or `..`. |
| `title` | string | yes |  |
| `tier` | string | yes | `standard_1080p` ($5 per stream-hour) or `pro_4k` ($20 per stream-hour). One of `"standard_1080p"`, `"pro_4k"`. |
| `state` | string | yes |  One of `"scheduled"`, `"starting"`, `"live"`, `"ended"`, `"failed"`. |
| `scheduled_start_at` | string (date-time) | no | When a booking starts itself. |
| `started_at` | string (date-time) | no |  |
| `ended_at` | string (date-time) | no |  |
| `unit_rate_usd_per_hour` | number | no | The stream-hour rate, fixed when it started. |
| `restream_unit_rate_usd_per_hour` | number | no | The destination-hour rate, fixed when it started. |
| `leg_count` | integer | yes | How many independent encodings the stream runs. |
| `legs_ready` | integer | yes | How many are encoding now. |
| `legs` | array of object | no | Each encoding's state, while the stream is not over. |
| `legs[].rank` | integer | yes |  |
| `legs[].state` | string | yes |  One of `"pending"`, `"ready"`, `"unhealthy"`. |
| `legs[].ready_since` | string (date-time) | no |  |
| `recording` | object | yes |  |
| `recording.state` | string | yes |  One of `"none"`, `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `recording.bytes` | integer (int64) | no |  |
| `recording.duration_seconds` | number | no |  |
| `recording.ready_at` | string (date-time) | no |  |
| `ingest` | object | no | Where an encoder sends the stream: RTMPS (RTMP inside TLS) on port 443. Ingest is encrypted only; plain RTMP is not accepted. The srt_ fields appear only where SRT is offered, and SRT is encrypted too: one passphrase per stream, shown with the key. |
| `ingest.key` | string | no | The ingest key. Only when it was just created or rotated. |
| `ingest.server_url` | string | no | The RTMPS server, for an encoder's Server field. |
| `ingest.stream_key` | string | no | The stream key, for the encoder's Stream Key field. Only with `key`. |
| `ingest.srt_url` | string | no | Only where SRT is offered. The whole SRT address for one-field encoders: stream ID, passphrase and AES-256. Only with `key`. |
| `ingest.srt_server` | string | no | Only where SRT is offered. The SRT server, srt://host:port. Not a secret. |
| `ingest.srt_stream_id` | string | no | Only where SRT is offered. publish:live/<playback_id>: names the broadcast, carries no key. Not a secret. |
| `ingest.srt_passphrase` | string | no | Only where SRT is offered. The stream's SRT passphrase: the encoder encrypts with it, and anything unencrypted or on another passphrase is refused. Only with `key`; a new key brings a new one. |
| `ingest.endpoints` | array of string | yes | Every RTMPS address the stream accepts. |
| `ingest.fanout_required` | boolean | yes | Whether the encoder must push to every endpoint for full redundancy. |
| `encoder_connected` | boolean | yes | Whether video is arriving from an encoder now. |
| `viewer_access` | object | yes |  |
| `viewer_access.visibility` | string | yes |  One of `"public"`, `"private"`. |
| `viewer_access.has_access_code` | boolean | yes |  |
| `viewer_access.interactions` | object | yes | What a stream's live room offers. |
| `viewer_access.interactions.chat` | boolean | yes |  |
| `viewer_access.interactions.reactions` | boolean | yes |  |
| `viewer_access.interactions.highlights` | boolean | yes | Gift highlights from your giving system. |
| `viewer_access.interactions.reaction_floats` | boolean | yes | Whether reactions float up the player. |
| `viewer_access.interactions.highlight_display` | boolean | yes | Whether highlights are shown in the player. |
| `viewer_access.access_code` | string | no | Present only in the response that set or generated the code. Never readable again. |
| `end_reason` | string or null | yes | Why the stream ended. Null until it has. One of `"customer_ended"`, `"no_video"`, `"max_duration"`, `"failed"`, `"not_started"`. |
| `start_failure` | string or null | yes | Why a booking could not start. Null unless it could not. One of `"cap_exceeded"`, `"already_on_air"`, `"no_rate"`, `"missed_window"`, `"org_inactive"`, `"payment_required"`. |
| `captions` | object | no |  |
| `captions.spoken_language` | string | yes | `auto`, or the language spoken: one of `ar de en es fr it ja ko pl pt ru tl uk vi zh`. |
| `captions.translation_languages` | array of string | yes | Reserved for translated captions. Empty. |
| `captions.translation_mode` | string | yes |  One of `"on_demand"`, `"always"`. |
| `captions.profanity_masking` | boolean | yes |  |
| `captions.sung_lyrics` | string | yes | Whether sung lyrics are captioned or shown as a music symbol. One of `"music-symbol"`, `"caption"`. |
| `captions.vocabulary` | array of string | yes | Up to 100 names and terms the captions should spell right. |
| `captions.retired_source_languages` | array of string | no |  |

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_body` | The body has a field that is not one of the documented ones. |
| 400 | `missing_field` | None of the fields was given. |
| 400 | `invalid_interactions` | `interactions` is not an object of the documented switches. |
| 400 | `invalid_viewer_access` | `visibility` or `access_code` is not acceptable; `detail` says why. |
| 400 | `invalid_captions` | `captions` is not acceptable; `detail` names the field. |
| 404 | `not_found` | No such stream in your organization. |
| 409 | `stream_ended` | The stream is over; its captions can no longer change. |

## Start a stream

`POST /v1/streams/{playback_id}/start`

Brings a booked stream up now: three legs start encoding and billing
begins. It goes live within about a minute of your encoder's video
arriving, while it gets ready. A stream left to start itself at its
booked time gets ready in the five minutes before, and goes live within
seconds of the start.
Calling it again on a stream already starting or live returns it
unchanged.

### Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes | The stream's `playback_id`. |

### Examples

cURL:

```bash
curl -X POST "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/start" \
  -H "Authorization: Bearer $CARDINAL_API_KEY"
```

Python:

```python
import os

import requests

response = requests.post(
    "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/start",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/start', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/start', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('POST', 'https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/start', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/start")
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("POST", "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/start", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(data))
}
```

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/start"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .method("POST", HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

```csharp
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("POST"), "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/start");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 200

The stream, now `starting` (or already `starting` or `live`).

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes | The stream's public id. Unguessable. |
| `customer_ref` | string | yes | Your identifier for a customer. 1-128 letters, digits, `.`, `_` or `-`; not `.` or `..`. |
| `title` | string | yes |  |
| `tier` | string | yes | `standard_1080p` ($5 per stream-hour) or `pro_4k` ($20 per stream-hour). One of `"standard_1080p"`, `"pro_4k"`. |
| `state` | string | yes |  One of `"scheduled"`, `"starting"`, `"live"`, `"ended"`, `"failed"`. |
| `scheduled_start_at` | string (date-time) | no | When a booking starts itself. |
| `started_at` | string (date-time) | no |  |
| `ended_at` | string (date-time) | no |  |
| `unit_rate_usd_per_hour` | number | no | The stream-hour rate, fixed when it started. |
| `restream_unit_rate_usd_per_hour` | number | no | The destination-hour rate, fixed when it started. |
| `leg_count` | integer | yes | How many independent encodings the stream runs. |
| `legs_ready` | integer | yes | How many are encoding now. |
| `legs` | array of object | no | Each encoding's state, while the stream is not over. |
| `legs[].rank` | integer | yes |  |
| `legs[].state` | string | yes |  One of `"pending"`, `"ready"`, `"unhealthy"`. |
| `legs[].ready_since` | string (date-time) | no |  |
| `recording` | object | yes |  |
| `recording.state` | string | yes |  One of `"none"`, `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `recording.bytes` | integer (int64) | no |  |
| `recording.duration_seconds` | number | no |  |
| `recording.ready_at` | string (date-time) | no |  |
| `ingest` | object | no | Where an encoder sends the stream: RTMPS (RTMP inside TLS) on port 443. Ingest is encrypted only; plain RTMP is not accepted. The srt_ fields appear only where SRT is offered, and SRT is encrypted too: one passphrase per stream, shown with the key. |
| `ingest.key` | string | no | The ingest key. Only when it was just created or rotated. |
| `ingest.server_url` | string | no | The RTMPS server, for an encoder's Server field. |
| `ingest.stream_key` | string | no | The stream key, for the encoder's Stream Key field. Only with `key`. |
| `ingest.srt_url` | string | no | Only where SRT is offered. The whole SRT address for one-field encoders: stream ID, passphrase and AES-256. Only with `key`. |
| `ingest.srt_server` | string | no | Only where SRT is offered. The SRT server, srt://host:port. Not a secret. |
| `ingest.srt_stream_id` | string | no | Only where SRT is offered. publish:live/<playback_id>: names the broadcast, carries no key. Not a secret. |
| `ingest.srt_passphrase` | string | no | Only where SRT is offered. The stream's SRT passphrase: the encoder encrypts with it, and anything unencrypted or on another passphrase is refused. Only with `key`; a new key brings a new one. |
| `ingest.endpoints` | array of string | yes | Every RTMPS address the stream accepts. |
| `ingest.fanout_required` | boolean | yes | Whether the encoder must push to every endpoint for full redundancy. |
| `encoder_connected` | boolean | yes | Whether video is arriving from an encoder now. |
| `viewer_access` | object | yes |  |
| `viewer_access.visibility` | string | yes |  One of `"public"`, `"private"`. |
| `viewer_access.has_access_code` | boolean | yes |  |
| `viewer_access.interactions` | object | yes | What a stream's live room offers. |
| `viewer_access.interactions.chat` | boolean | yes |  |
| `viewer_access.interactions.reactions` | boolean | yes |  |
| `viewer_access.interactions.highlights` | boolean | yes | Gift highlights from your giving system. |
| `viewer_access.interactions.reaction_floats` | boolean | yes | Whether reactions float up the player. |
| `viewer_access.interactions.highlight_display` | boolean | yes | Whether highlights are shown in the player. |
| `viewer_access.access_code` | string | no | Present only in the response that set or generated the code. Never readable again. |
| `end_reason` | string or null | yes | Why the stream ended. Null until it has. One of `"customer_ended"`, `"no_video"`, `"max_duration"`, `"failed"`, `"not_started"`. |
| `start_failure` | string or null | yes | Why a booking could not start. Null unless it could not. One of `"cap_exceeded"`, `"already_on_air"`, `"no_rate"`, `"missed_window"`, `"org_inactive"`, `"payment_required"`. |
| `captions` | object | no |  |
| `captions.spoken_language` | string | yes | `auto`, or the language spoken: one of `ar de en es fr it ja ko pl pt ru tl uk vi zh`. |
| `captions.translation_languages` | array of string | yes | Reserved for translated captions. Empty. |
| `captions.translation_mode` | string | yes |  One of `"on_demand"`, `"always"`. |
| `captions.profanity_masking` | boolean | yes |  |
| `captions.sung_lyrics` | string | yes | Whether sung lyrics are captioned or shown as a music symbol. One of `"music-symbol"`, `"caption"`. |
| `captions.vocabulary` | array of string | yes | Up to 100 names and terms the captions should spell right. |
| `captions.retired_source_languages` | array of string | no |  |

### Errors

| Status | Code | Meaning |
|---|---|---|
| 402 | `cap_exceeded` | Your organization has reached the monthly stream-hour cap set on its account. |
| 402 | `payment_required` | Streaming is paused: a bill is unpaid after its seven days. Pay it in the console (Billing); streaming resumes at once. |
| 404 | `not_found` | No such stream in your organization. |
| 409 | `already_active` | This customer already has a stream on air and your organization limits that. |
| 409 | `stream_ended` | The stream is over and cannot be started again. Book a new one. |
| 503 | `no_rate` | No price is configured for this tier. Contact us. |

## End a stream

`POST /v1/streams/{playback_id}/end`

Takes a stream down and stops billing. Its recording starts to be made.
Calling it again returns the same answer and bills nothing more. A
booking that never started ends with `billed_usd: 0`.

### Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes | The stream's `playback_id`. |

### Examples

cURL:

```bash
curl -X POST "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/end" \
  -H "Authorization: Bearer $CARDINAL_API_KEY"
```

Python:

```python
import os

import requests

response = requests.post(
    "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/end",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/end', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/end', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('POST', 'https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/end', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/end")
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("POST", "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/end", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(data))
}
```

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/end"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .method("POST", HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

```csharp
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("POST"), "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/end");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 200

The ended stream and what it bills.

### Errors

| Status | Code | Meaning |
|---|---|---|
| 404 | `not_found` | No such stream in your organization. |

## Rotate the ingest key

`POST /v1/streams/{playback_id}/ingest-key`

Replaces a stream's ingest key and returns the new address. The old key
stops working immediately. Use it if a key leaks, or to get a key for a
stream booked by a retried request.

### Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes | The stream's `playback_id`. |

### Examples

cURL:

```bash
curl -X POST "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/ingest-key" \
  -H "Authorization: Bearer $CARDINAL_API_KEY"
```

Python:

```python
import os

import requests

response = requests.post(
    "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/ingest-key",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/ingest-key', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/ingest-key', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('POST', 'https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/ingest-key', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/ingest-key")
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("POST", "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/ingest-key", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(data))
}
```

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/ingest-key"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .method("POST", HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

```csharp
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("POST"), "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/ingest-key");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 200

The new ingest address and key.

| Name | Type | Required | Description |
|---|---|---|---|
| `key` | string | no | The ingest key. Only when it was just created or rotated. |
| `server_url` | string | no | The RTMPS server, for an encoder's Server field. |
| `stream_key` | string | no | The stream key, for the encoder's Stream Key field. Only with `key`. |
| `srt_url` | string | no | Only where SRT is offered. The whole SRT address for one-field encoders: stream ID, passphrase and AES-256. Only with `key`. |
| `srt_server` | string | no | Only where SRT is offered. The SRT server, srt://host:port. Not a secret. |
| `srt_stream_id` | string | no | Only where SRT is offered. publish:live/<playback_id>: names the broadcast, carries no key. Not a secret. |
| `srt_passphrase` | string | no | Only where SRT is offered. The stream's SRT passphrase: the encoder encrypts with it, and anything unencrypted or on another passphrase is refused. Only with `key`; a new key brings a new one. |
| `endpoints` | array of string | yes | Every RTMPS address the stream accepts. |
| `fanout_required` | boolean | yes | Whether the encoder must push to every endpoint for full redundancy. |

### Errors

| Status | Code | Meaning |
|---|---|---|
| 404 | `not_found` | No such stream in your organization, or it is over. |
