Recordings API
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.
This page is generated from the OpenAPI specification; the same text is inMarkdown for tools and AI assistants.
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 | Description |
|---|---|---|
| playback_idrequired | string | The stream’s |
Query parameters
| Name | Type | Description |
|---|---|---|
| link_ttl | integer | Seconds each download link lives, 60 to 604800 (7 days). Default 3600. A link never outlives the recording. |
Response 200
The recording.
Fields
| Name | Type | Description |
|---|---|---|
| playback_idrequired | string | |
| staterequired | string |
One of |
| bytesrequired | integer (int64) or null | |
| duration_secondsrequired | number or null | |
| missing_secondsrequired | number or null | How much of the broadcast is not in the file; 0 when complete. |
| ready_atrequired | string (date-time) or null | |
| retained_untilrequired | string (date-time) or null | When the file will be deleted if you do not delete it first. |
| download_url | string | Only when |
| expires_at | string (date-time) | When |
| failure_code | string | Only when |
| delete_requested_at | string (date-time) | |
| deleted_at | string (date-time) |
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_link_ttl |
|
| 404 | not_found | No such stream in your organization. |
curl "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/recording?link_ttl=86400" \ -H "Authorization: Bearer $CARDINAL_API_KEY"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)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);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);<?phprequire '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;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.bodypackage 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))}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()); }}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()}");{ "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"}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 | Description |
|---|---|---|
| playback_idrequired | string | The stream’s |
Response 200
Deleted (or there was nothing to delete).
Fields
| Name | Type | Description |
|---|---|---|
| playback_idrequired | string | |
| staterequired | string |
One of |
| bytesrequired | integer (int64) or null | |
| duration_secondsrequired | number or null | |
| missing_secondsrequired | number or null | How much of the broadcast is not in the file; 0 when complete. |
| ready_atrequired | string (date-time) or null | |
| retained_untilrequired | string (date-time) or null | When the file will be deleted if you do not delete it first. |
| download_url | string | Only when |
| expires_at | string (date-time) | When |
| failure_code | string | Only when |
| delete_requested_at | string (date-time) | |
| deleted_at | string (date-time) |
Response 202
Deleting.
Fields
| Name | Type | Description |
|---|---|---|
| playback_idrequired | string | |
| staterequired | string |
One of |
| bytesrequired | integer (int64) or null | |
| duration_secondsrequired | number or null | |
| missing_secondsrequired | number or null | How much of the broadcast is not in the file; 0 when complete. |
| ready_atrequired | string (date-time) or null | |
| retained_untilrequired | string (date-time) or null | When the file will be deleted if you do not delete it first. |
| download_url | string | Only when |
| expires_at | string (date-time) | When |
| failure_code | string | Only when |
| delete_requested_at | string (date-time) | |
| deleted_at | string (date-time) |
Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found | No such stream in your organization. |
| 409 | stream_not_ended | End the stream before deleting its recording. |
curl -X DELETE "https://api.cardinalstreaming.com/v1/streams/1846bdac2d8c0ce0e846d4a3817012c9/recording" \ -H "Authorization: Bearer $CARDINAL_API_KEY"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)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);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);<?phprequire '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;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.bodypackage 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))}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()); }}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()}");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 | Description |
|---|---|---|
| customer_ref | string | Only this customer’s recordings. |
| state | string | Only recordings in this state. One of |
| ended_after | string (date-time) | Only broadcasts that ended at or after this time (RFC 3339). |
| ended_before | string (date-time) | Only broadcasts that ended before this time (RFC 3339). |
| limit | integer | Page size, 1-200. Default 50; more than 200 is treated as 200. |
| cursor | string | The previous page’s |
| link_ttl | integer | Seconds each download link lives, 60 to 604800 (7 days). Default 3600. A link never outlives the recording. |
Response 200
A page of recordings.
Fields
| Name | Type | Description |
|---|---|---|
| recordingsrequired | array of any | |
| next_cursor | string | Present when there is another page. Pass it as |
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_customer_ref |
|
| 400 | invalid_state |
|
| 400 | invalid_ended_after |
|
| 400 | invalid_ended_before |
|
| 400 | invalid_limit |
|
| 400 | invalid_cursor |
|
| 400 | invalid_link_ttl |
|
curl "https://api.cardinalstreaming.com/v1/recordings?customer_ref=grace-chapel&state=ready&link_ttl=86400" \ -H "Authorization: Bearer $CARDINAL_API_KEY"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)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);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);<?phprequire '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;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.bodypackage 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))}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()); }}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()}");{ "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"}List one customer's recordings
GET/v1/customers/{customer_ref}/recordings
The same as List recordings for one customer. A customer with no recordings, or one you never used, is an empty list.
Path parameters
| Name | Type | Description |
|---|---|---|
| customer_refrequired | string | Your identifier for the customer. |
Query parameters
| Name | Type | Description |
|---|---|---|
| state | string | Only recordings in this state. One of |
| ended_after | string (date-time) | Only broadcasts that ended at or after this time (RFC 3339). |
| ended_before | string (date-time) | Only broadcasts that ended before this time (RFC 3339). |
| limit | integer | Page size, 1-200. Default 50; more than 200 is treated as 200. |
| cursor | string | The previous page’s |
| link_ttl | integer | Seconds each download link lives, 60 to 604800 (7 days). Default 3600. A link never outlives the recording. |
Response 200
A page of the customer’s recordings.
Fields
| Name | Type | Description |
|---|---|---|
| recordingsrequired | array of any | |
| next_cursor | string | Present when there is another page. Pass it as |
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_customer_ref | The path’s customer reference is not valid. |
| 400 | invalid_state |
|
| 400 | invalid_ended_after |
|
| 400 | invalid_ended_before |
|
| 400 | invalid_limit |
|
| 400 | invalid_cursor |
|
| 400 | invalid_link_ttl |
|
curl "https://api.cardinalstreaming.com/v1/customers/grace-chapel/recordings?state=ready&link_ttl=86400" \ -H "Authorization: Bearer $CARDINAL_API_KEY"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)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);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);<?phprequire '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;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.bodypackage 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))}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()); }}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()}");{ "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"}