# Live room

Chat, reactions and gift highlights beside a stream. Your server mints a
pass for each viewer or moderator, posts gift highlights from your own
giving system, and moderates.

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

## Get a live room pass

`GET /v1/streams/{playback_id}/room`

A pass into the stream's live room (chat, reactions, highlights) for a
viewer or a moderator: a WebSocket URL and a token, valid six hours for
a viewer and four for a moderator. A viewer can join only while the
stream is live. Name a viewer you have signed in with `X-Viewer-Name`.

### Path parameters

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

### Query parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `role` | string | no | `viewer` (the default) or `moderator`. One of `"viewer"`, `"moderator"`. |
| `viewer` | string | no | `1` to check a viewer's access code for a private stream, as for playback. One of `"1"`, `"true"`. |
| `access_code` | string | no | The viewer's access code for a private stream (or send `X-Access-Code`). |
| `viewer_ip` | string | no | The viewer's IP address (or send `X-Viewer-IP`). |

### Headers

| Name | Type | Required | Description |
|---|---|---|---|
| `X-Viewer-Name` | string | no | The name of a viewer you have signed in, percent-encoded UTF-8, 1-40 characters. Shown in the room. Send it only for a viewer whose identity you have verified. |
| `X-Viewer-Member` | string | no | `1` to mark the named viewer as a member of the customer's community. Only with `X-Viewer-Name`. One of `"1"`. |
| `X-Access-Code` | string | no | The viewer's access code for a private stream. |
| `X-Viewer-IP` | string | no | The viewer's IP address, so wrong access codes are limited per viewer. |

### Examples

cURL:

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

Python:

```python
import os

import requests

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

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/room?role=moderator', {
  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/room?role=moderator', {
  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/room', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'query' => [
        'role' => 'moderator',
    ],
    '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/room?role=moderator")
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/room?role=moderator", 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/room?role=moderator"))
            .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/room?role=moderator");
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 pass.

```json
{
  "url": "wss://live.cardinal.stream/rooms/1846bdac2d8c0ce0e846d4a3817012c9",
  "token": "v1.eyJy...",
  "expires_at": "2026-09-20T19:41:07Z",
  "role": "moderator",
  "viewer_id": "q2Vt3b0yJ1tF6pXhR8m4Ag"
}
```

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_role` | `role` is not `viewer` or `moderator`, or a viewer check asked for a moderator pass. |
| 400 | `invalid_viewer` | `viewer` is not `1` or `true`. |
| 400 | `invalid_viewer_name` | `X-Viewer-Name` is not a 1-40 character name, percent-encoded. |
| 400 | `invalid_viewer_member` | `X-Viewer-Member` is not `1`, or was sent without `X-Viewer-Name`. |
| 403 | `access_code_required` | The stream is private and no access code was given. |
| 403 | `access_code_invalid` | That access code is not this stream's. |
| 404 | `not_found` | No such stream in your organization. |
| 409 | `not_live` | A viewer can join only while the stream is live. |
| 429 | `too_many_attempts` | Too many wrong access codes from this viewer. |
| 503 | `live_unconfigured` | The live room is not available on this deployment. |

## Show a gift in the room

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

Shows a gift from your own giving system to everyone in the room, while
the stream is live. Cardinal takes no payments: post a highlight after
your giving system has taken the gift. `id` is yours and makes the call
safe to retry: a repeat is answered `200` with `duplicate: true` and
shown once. `message` is accepted and discarded -- it reaches no viewer.

### Path parameters

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

### Request body

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Your id for the gift. A repeat is shown once. |
| `kind` | string | yes |  One of `"gift"`. |
| `amount_display` | string | yes | Shown exactly as given, such as `$50`. |
| `name` | string | no | The giver, only if they chose to be shown. |
| `message` | string | no | Accepted and discarded. |

### Examples

cURL:

```bash
curl -X POST "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/highlights" \
  -H "Authorization: Bearer $CARDINAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "gift-5f1c0a92",
  "kind": "gift",
  "amount_display": "$50",
  "name": "The Alvarez family"
}'
```

Python:

```python
import os

import requests

response = requests.post(
    "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/highlights",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    json={
        "id": "gift-5f1c0a92",
        "kind": "gift",
        "amount_display": "$50",
        "name": "The Alvarez family",
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/highlights', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "id": "gift-5f1c0a92",
    "kind": "gift",
    "amount_display": "$50",
    "name": "The Alvarez family"
  }),
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/highlights', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "id": "gift-5f1c0a92",
    "kind": "gift",
    "amount_display": "$50",
    "name": "The Alvarez family"
  }),
});
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/highlights', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'json' => [
        'id' => 'gift-5f1c0a92',
        'kind' => 'gift',
        'amount_display' => '$50',
        'name' => 'The Alvarez family',
    ],
    '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/highlights")
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
  "id" => "gift-5f1c0a92",
  "kind" => "gift",
  "amount_display" => "$50",
  "name" => "The Alvarez family"
})

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/1846bdac2d8c0ce0e846d4a3817012c9/highlights", strings.NewReader(`{
  "id": "gift-5f1c0a92",
  "kind": "gift",
  "amount_display": "$50",
  "name": "The Alvarez family"
}`))
	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/highlights"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .header("Content-Type", "application/json")
            .method("POST", HttpRequest.BodyPublishers.ofString("{\"id\":\"gift-5f1c0a92\",\"kind\":\"gift\",\"amount_display\":\"$50\",\"name\":\"The Alvarez family\"}"))
            .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/highlights");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));
request.Content = new StringContent("{\"id\":\"gift-5f1c0a92\",\"kind\":\"gift\",\"amount_display\":\"$50\",\"name\":\"The Alvarez family\"}", Encoding.UTF8, "application/json");

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

### Response 200

A repeat of an `id` already shown; nothing new was shown.

| Name | Type | Required | Description |
|---|---|---|---|
| `delivered` | integer | yes | How many connected viewers it reached. |
| `duplicate` | boolean | yes |  |
| `displayed` | boolean | no |  |

### Response 202

Delivered to the room (`displayed` is `false` when gifts are on but their display is switched off).

| Name | Type | Required | Description |
|---|---|---|---|
| `delivered` | integer | yes | How many connected viewers it reached. |
| `duplicate` | boolean | yes |  |
| `displayed` | boolean | no |  |

```json
{
  "delivered": 312,
  "duplicate": false
}
```

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_body` | The body has a field that is not one of the documented ones. |
| 400 | `unknown_kind` | `kind` is not `gift`. |
| 400 | `invalid_id` | `id` is not 1-80 letters, digits, `.`, `:`, `_` or `-`. |
| 400 | `invalid_amount_display` | `amount_display` is not 1-24 characters. |
| 404 | `not_found` | No such stream in your organization. |
| 409 | `not_live` | Highlights can be posted only while the stream is live. |
| 409 | `highlights_off` | Highlights are switched off for this stream. |
| 502 | `room_unreachable` | The live room did not answer; retry shortly. |
| 503 | `live_unconfigured` | The live room is not available on this deployment. |

## Moderate the room

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

Removes a chat message, or mutes or unmutes a viewer (optionally removing
everything they have said). The ids come from the room's own messages.

### Path parameters

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

### Request body

| Name | Type | Required | Description |
|---|---|---|---|
| `action` | string | yes |  One of `"remove_message"`, `"mute_viewer"`, `"unmute_viewer"`. |
| `message_id` | string | no | For `remove_message`. |
| `viewer_id` | string | no | For `mute_viewer` and `unmute_viewer`. |
| `remove_messages` | boolean | no | With `mute_viewer`, also remove what they have said. |

### Examples

cURL:

```bash
curl -X POST "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/moderation" \
  -H "Authorization: Bearer $CARDINAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "action": "mute_viewer",
  "viewer_id": "q2Vt3b0yJ1tF6pXhR8m4Ag",
  "remove_messages": true
}'
```

Python:

```python
import os

import requests

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

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/moderation', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "action": "mute_viewer",
    "viewer_id": "q2Vt3b0yJ1tF6pXhR8m4Ag",
    "remove_messages": true
  }),
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/moderation', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "action": "mute_viewer",
    "viewer_id": "q2Vt3b0yJ1tF6pXhR8m4Ag",
    "remove_messages": true
  }),
});
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/moderation', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'json' => [
        'action' => 'mute_viewer',
        'viewer_id' => 'q2Vt3b0yJ1tF6pXhR8m4Ag',
        'remove_messages' => true,
    ],
    '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/moderation")
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
  "action" => "mute_viewer",
  "viewer_id" => "q2Vt3b0yJ1tF6pXhR8m4Ag",
  "remove_messages" => true
})

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/1846bdac2d8c0ce0e846d4a3817012c9/moderation", strings.NewReader(`{
  "action": "mute_viewer",
  "viewer_id": "q2Vt3b0yJ1tF6pXhR8m4Ag",
  "remove_messages": true
}`))
	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/moderation"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .header("Content-Type", "application/json")
            .method("POST", HttpRequest.BodyPublishers.ofString("{\"action\":\"mute_viewer\",\"viewer_id\":\"q2Vt3b0yJ1tF6pXhR8m4Ag\",\"remove_messages\":true}"))
            .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/moderation");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));
request.Content = new StringContent("{\"action\":\"mute_viewer\",\"viewer_id\":\"q2Vt3b0yJ1tF6pXhR8m4Ag\",\"remove_messages\":true}", Encoding.UTF8, "application/json");

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

### Response 200

What the room did.

| Name | Type | Required | Description |
|---|---|---|---|
| `removed` | one of | no | Whether the message was removed, or how many messages a mute removed. |
| `muted` | boolean | no |  |
| `unmuted` | boolean | no |  |
| `duplicate` | boolean | no |  |
| `repeated` | boolean | no |  |

```json
{
  "muted": true,
  "removed": 3
}
```

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_body` | The body has a field that is not one of the documented ones. |
| 400 | `unknown_action` | `action` is not `remove_message`, `mute_viewer` or `unmute_viewer`. |
| 400 | `invalid_message_id` | `message_id` is missing for `remove_message`. |
| 400 | `invalid_viewer_id` | `viewer_id` is missing for a mute or unmute. |
| 404 | `not_found` | No such stream in your organization. |
| 502 | `room_unreachable` | The live room did not answer; retry shortly. |
| 503 | `live_unconfigured` | The live room is not available on this deployment. |
