# Recordings

Every broadcast is recorded as one MP4. Collect it within 30 days -- it is
then deleted for good -- by downloading it or by handing its link to
another system. Downloads are free, resumable and fast on a single
connection.

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

## Read a recording

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

A stream's recording and, once it is `ready`, a link to download it.
The link is signed for this one file, minted fresh on every read, and
lives an hour unless you ask for longer with `link_ttl` (up to 7 days,
never past the recording's deletion). Use a long link to hand the file
to another system that imports it from a URL.

Downloads are free, support `Range` requests for resuming, and run at
your connection's speed on a single connection.

### Path parameters

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

### Query parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `link_ttl` | integer | no | Seconds each download link lives, 60 to 604800 (7 days). Default 3600. A link never outlives the recording. |

### Examples

cURL:

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

Python:

```python
import os

import requests

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

JavaScript:

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

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes |  |
| `state` | string | yes | `none`: nothing to collect yet. `finalizing`, `exporting`: being made (about a minute per hour of broadcast). `ready`: the file exists. `failed`: it could not be made. `deleting`, `deleted`: you deleted it. `expired`: its 30 days ran out. One of `"none"`, `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `bytes` | integer (int64) or null | yes |  |
| `duration_seconds` | number or null | yes |  |
| `missing_seconds` | number or null | yes | How much of the broadcast is not in the file; 0 when complete. |
| `ready_at` | string (date-time) or null | yes |  |
| `retained_until` | string (date-time) or null | yes | When the file will be deleted if you do not delete it first. |
| `download_url` | string | no | Only when `ready`. A credential for this one file. |
| `expires_at` | string (date-time) | no | When `download_url` stops working. |
| `failure_code` | string | no | Only when `failed`: `no_media`, `unreadable_media` or `export_failed`. |
| `delete_requested_at` | string (date-time) | no |  |
| `deleted_at` | string (date-time) | no |  |

```json
{
  "playback_id": "1846bdac2d8c0ce0e846d4a3817012c9",
  "state": "ready",
  "bytes": 2893012004,
  "duration_seconds": 5412.48,
  "missing_seconds": 0,
  "ready_at": "2026-09-20T15:41:07Z",
  "retained_until": "2026-10-20T15:41:07Z",
  "download_url": "https://media.cardinal.stream/1846bdac2d8c0ce0e846d4a3817012c9/recording.mp4?t=v1.eyJw...",
  "expires_at": "2026-09-20T16:43:12Z"
}
```

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_link_ttl` | `link_ttl` is not a whole number of seconds from 60 to 604800. |
| 404 | `not_found` | No such stream in your organization. |

## Delete a recording

`DELETE /v1/streams/{playback_id}/recording`

Deletes the MP4 and every segment of the broadcast, for good, once you
have your copy. Only after the stream has ended. Answers `202` while the
deletion runs and `200` once it is done; calling it again is safe.

### Path parameters

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

### Examples

cURL:

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

Python:

```python
import os

import requests

response = requests.delete(
    "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/recording",
    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/recording', {
  method: 'DELETE',
  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/recording', {
  method: 'DELETE',
  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('DELETE', 'https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/recording', [
    '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/recording")
request = Net::HTTP::Delete.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("DELETE", "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/recording", 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/recording"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .method("DELETE", 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("DELETE"), "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/recording");
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

Deleted (or there was nothing to delete).

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes |  |
| `state` | string | yes | `none`: nothing to collect yet. `finalizing`, `exporting`: being made (about a minute per hour of broadcast). `ready`: the file exists. `failed`: it could not be made. `deleting`, `deleted`: you deleted it. `expired`: its 30 days ran out. One of `"none"`, `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `bytes` | integer (int64) or null | yes |  |
| `duration_seconds` | number or null | yes |  |
| `missing_seconds` | number or null | yes | How much of the broadcast is not in the file; 0 when complete. |
| `ready_at` | string (date-time) or null | yes |  |
| `retained_until` | string (date-time) or null | yes | When the file will be deleted if you do not delete it first. |
| `download_url` | string | no | Only when `ready`. A credential for this one file. |
| `expires_at` | string (date-time) | no | When `download_url` stops working. |
| `failure_code` | string | no | Only when `failed`: `no_media`, `unreadable_media` or `export_failed`. |
| `delete_requested_at` | string (date-time) | no |  |
| `deleted_at` | string (date-time) | no |  |

### Response 202

Deleting.

| Name | Type | Required | Description |
|---|---|---|---|
| `playback_id` | string | yes |  |
| `state` | string | yes | `none`: nothing to collect yet. `finalizing`, `exporting`: being made (about a minute per hour of broadcast). `ready`: the file exists. `failed`: it could not be made. `deleting`, `deleted`: you deleted it. `expired`: its 30 days ran out. One of `"none"`, `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `bytes` | integer (int64) or null | yes |  |
| `duration_seconds` | number or null | yes |  |
| `missing_seconds` | number or null | yes | How much of the broadcast is not in the file; 0 when complete. |
| `ready_at` | string (date-time) or null | yes |  |
| `retained_until` | string (date-time) or null | yes | When the file will be deleted if you do not delete it first. |
| `download_url` | string | no | Only when `ready`. A credential for this one file. |
| `expires_at` | string (date-time) | no | When `download_url` stops working. |
| `failure_code` | string | no | Only when `failed`: `no_media`, `unreadable_media` or `export_failed`. |
| `delete_requested_at` | string (date-time) | no |  |
| `deleted_at` | string (date-time) | no |  |

### Errors

| Status | Code | Meaning |
|---|---|---|
| 404 | `not_found` | No such stream in your organization. |
| 409 | `stream_not_ended` | End the stream before deleting its recording. |

## List recordings

`GET /v1/recordings`

Every recording you have, most recently **ended** first, each ready one
with its own fresh download link -- the one call for moving every
customer's recordings into your own storage. Page with `cursor` until
`next_cursor` is absent. Only broadcasts that have a recording are listed.

### Query parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `customer_ref` | string | no | Only this customer's recordings. |
| `state` | string | no | Only recordings in this state. `ready` for those with a file to collect. One of `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `ended_after` | string | no | Only broadcasts that ended at or after this time (RFC 3339). |
| `ended_before` | string | no | Only broadcasts that ended before this time (RFC 3339). |
| `limit` | integer | no | Page size, 1-200. Default 50; more than 200 is treated as 200. |
| `cursor` | string | no | The previous page's `next_cursor`. |
| `link_ttl` | integer | no | Seconds each download link lives, 60 to 604800 (7 days). Default 3600. A link never outlives the recording. |

### Examples

cURL:

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

Python:

```python
import os

import requests

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

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/recordings?customer_ref=grace-chapel&state=ready&link_ttl=86400', {
  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/recordings?customer_ref=grace-chapel&state=ready&link_ttl=86400', {
  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/recordings', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'query' => [
        'customer_ref' => 'grace-chapel',
        'state' => 'ready',
        'link_ttl' => '86400',
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

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

uri = URI("https://api.cardinalstreaming.com/v1/recordings?customer_ref=grace-chapel&state=ready&link_ttl=86400")
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/recordings?customer_ref=grace-chapel&state=ready&link_ttl=86400", 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/recordings?customer_ref=grace-chapel&state=ready&link_ttl=86400"))
            .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/recordings?customer_ref=grace-chapel&state=ready&link_ttl=86400");
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 recordings.

| Name | Type | Required | Description |
|---|---|---|---|
| `recordings` | array of any | yes |  |
| `next_cursor` | string | no | Present when there is another page. Pass it as `cursor`. |

```json
{
  "recordings": [
    {
      "customer_ref": "grace-chapel",
      "title": "Sunday Service",
      "tier": "standard_1080p",
      "ended_at": "2026-09-20T15:39:58Z",
      "playback_id": "1846bdac2d8c0ce0e846d4a3817012c9",
      "state": "ready",
      "bytes": 2893012004,
      "duration_seconds": 5412.48,
      "missing_seconds": 0,
      "ready_at": "2026-09-20T15:41:07Z",
      "retained_until": "2026-10-20T15:41:07Z",
      "download_url": "https://media.cardinal.stream/1846bdac2d8c0ce0e846d4a3817012c9/recording.mp4?t=v1.eyJw...",
      "expires_at": "2026-09-21T15:43:12Z"
    }
  ],
  "next_cursor": "cjE6MjAyNi0wOS0yMFQxNTozOTo1OFp8MTg0NmJkYWM"
}
```

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_customer_ref` | `customer_ref` is not a valid customer reference. |
| 400 | `invalid_state` | `state` is not one of the recording states (other than `none`). |
| 400 | `invalid_ended_after` | `ended_after` is not an RFC 3339 time. |
| 400 | `invalid_ended_before` | `ended_before` is not an RFC 3339 time. |
| 400 | `invalid_limit` | `limit` is not a positive whole number. |
| 400 | `invalid_cursor` | `cursor` is not a `next_cursor` from a previous page. |
| 400 | `invalid_link_ttl` | `link_ttl` is not a whole number of seconds from 60 to 604800. |

## List one customer's recordings

`GET /v1/customers/{customer_ref}/recordings`

The same as [List recordings](#listRecordings) for one customer. A
customer with no recordings, or one you never used, is an empty list.

### Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `customer_ref` | string | yes | Your identifier for the customer. |

### Query parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `state` | string | no | Only recordings in this state. `ready` for those with a file to collect. One of `"finalizing"`, `"exporting"`, `"ready"`, `"failed"`, `"deleting"`, `"deleted"`, `"expired"`. |
| `ended_after` | string | no | Only broadcasts that ended at or after this time (RFC 3339). |
| `ended_before` | string | no | Only broadcasts that ended before this time (RFC 3339). |
| `limit` | integer | no | Page size, 1-200. Default 50; more than 200 is treated as 200. |
| `cursor` | string | no | The previous page's `next_cursor`. |
| `link_ttl` | integer | no | Seconds each download link lives, 60 to 604800 (7 days). Default 3600. A link never outlives the recording. |

### Examples

cURL:

```bash
curl "https://api.cardinalstreaming.com/v1/customers/grace-chapel/recordings?state=ready&link_ttl=86400" \
  -H "Authorization: Bearer $CARDINAL_API_KEY"
```

Python:

```python
import os

import requests

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

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/customers/grace-chapel/recordings?state=ready&link_ttl=86400', {
  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/customers/grace-chapel/recordings?state=ready&link_ttl=86400', {
  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/customers/grace-chapel/recordings', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'query' => [
        'state' => 'ready',
        'link_ttl' => '86400',
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

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

uri = URI("https://api.cardinalstreaming.com/v1/customers/grace-chapel/recordings?state=ready&link_ttl=86400")
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/customers/grace-chapel/recordings?state=ready&link_ttl=86400", 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/customers/grace-chapel/recordings?state=ready&link_ttl=86400"))
            .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/customers/grace-chapel/recordings?state=ready&link_ttl=86400");
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 the customer's recordings.

| Name | Type | Required | Description |
|---|---|---|---|
| `recordings` | array of any | yes |  |
| `next_cursor` | string | no | Present when there is another page. Pass it as `cursor`. |

```json
{
  "recordings": [
    {
      "customer_ref": "grace-chapel",
      "title": "Sunday Service",
      "tier": "standard_1080p",
      "ended_at": "2026-09-20T15:39:58Z",
      "playback_id": "1846bdac2d8c0ce0e846d4a3817012c9",
      "state": "ready",
      "bytes": 2893012004,
      "duration_seconds": 5412.48,
      "missing_seconds": 0,
      "ready_at": "2026-09-20T15:41:07Z",
      "retained_until": "2026-10-20T15:41:07Z",
      "download_url": "https://media.cardinal.stream/1846bdac2d8c0ce0e846d4a3817012c9/recording.mp4?t=v1.eyJw...",
      "expires_at": "2026-09-21T15:43:12Z"
    }
  ],
  "next_cursor": "cjE6MjAyNi0wOS0yMFQxNTozOTo1OFp8MTg0NmJkYWM"
}
```

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_customer_ref` | The path's customer reference is not valid. |
| 400 | `invalid_state` | `state` is not one of the recording states (other than `none`). |
| 400 | `invalid_ended_after` | `ended_after` is not an RFC 3339 time. |
| 400 | `invalid_ended_before` | `ended_before` is not an RFC 3339 time. |
| 400 | `invalid_limit` | `limit` is not a positive whole number. |
| 400 | `invalid_cursor` | `cursor` is not a `next_cursor` from a previous page. |
| 400 | `invalid_link_ttl` | `link_ttl` is not a whole number of seconds from 60 to 604800. |
