{
  "openapi": "3.0.3",
  "info": {
    "title": "Cardinal API",
    "version": "1.0",
    "description": "Live streaming over a REST API: book a broadcast, give an encoder its\naddress, play it to any number of viewers, and collect the recording.\n\nJSON over HTTPS. Every request is authenticated with an API key as a\nbearer token: `Authorization: Bearer strm_...`. A key identifies one\norganization, which is what a stream is billed to. Keep keys on your\nservers, never in a browser or an app.\n\nErrors share one shape: `{\"error\": \"<code>\", \"detail\": \"<why, for a person>\"}`.\nBranch on `error`; show or log `detail`. A missing, unknown or revoked key\nis `401 unauthenticated`. Another organization's resource is `404\nnot_found`, never `403`.\n\nThis description is held to the API by the API's own tests: every route\nis described, nothing is described that does not exist, and every response\nthe tests receive is checked against it.\n"
  },
  "servers": [
    {
      "url": "https://api.cardinalstreaming.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "tags": [
    {
      "name": "Streams",
      "x-slug": "streams",
      "description": "A stream is one broadcast. Book it (now or for later), point an encoder\nat its ingest address, start it, and end it. A stream is billed per\nminute from when it starts to when it ends, with a 15-minute minimum.\n"
    },
    {
      "name": "Playback",
      "x-slug": "playback",
      "description": "A short-lived, signed URL a player loads to watch a stream live or, once\nit has ended, its recording. Mint one per viewer session from your\nserver; never cache one for everyone.\n"
    },
    {
      "name": "Player",
      "x-slug": "player",
      "description": "Your brand on the hosted watch page and embed at cardinal.stream: name,\nlogo, color, theme, an optional giving button, and the Cardinal badge.\n"
    },
    {
      "name": "Live room",
      "x-slug": "live-room",
      "description": "Chat, reactions and gift highlights beside a stream. Your server mints a\npass for each viewer or moderator, posts gift highlights from your own\ngiving system, and moderates.\n"
    },
    {
      "name": "Recordings",
      "x-slug": "recordings",
      "description": "Every broadcast is recorded as one MP4. Collect it within 30 days -- it is\nthen deleted for good -- by downloading it or by handing its link to\nanother system. Downloads are free, resumable and fast on a single\nconnection.\n"
    },
    {
      "name": "Customers",
      "x-slug": "customers",
      "description": "If you stream for your own customers (you are a partner), each stream\nnames its customer with `customer_ref`, your own identifier. Booking a\nstream registers its customer; these endpoints name, list and archive\nthem, and pull their recordings.\n"
    },
    {
      "name": "Restreaming",
      "x-slug": "restreaming",
      "description": "Send a live stream on to up to five other services that take RTMP or\nRTMPS, at the same time. Each destination is billed per destination-hour\nwhile it is live.\n"
    },
    {
      "name": "Captions",
      "x-slug": "captions",
      "description": "Live captions in the language being spoken, included with every stream.\nThese endpoints answer `404` until live captions are enabled for your\norganization.\n"
    },
    {
      "name": "Usage",
      "x-slug": "usage",
      "description": "What was streamed in a month and what it cost, stream by stream, and per\ncustomer -- what a partner bills its own customers from.\n"
    },
    {
      "name": "Billing",
      "x-slug": "billing",
      "description": "Your invoices, the card on file, and paying an invoice now. These work\neven while streaming is paused for an unpaid bill, so the bill can\nalways be paid.\n"
    },
    {
      "name": "Health",
      "x-slug": "health",
      "description": "Whether the API is up. No key needed."
    }
  ],
  "paths": {
    "/healthz": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Health"
        ],
        "summary": "Is the API up",
        "description": "Answers as long as the process is running. Does not touch the database.",
        "security": [],
        "responses": {
          "200": {
            "description": "The API is up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    }
                  }
                },
                "example": {
                  "status": "ok"
                }
              }
            }
          }
        }
      }
    },
    "/readyz": {
      "get": {
        "operationId": "getReady",
        "tags": [
          "Health"
        ],
        "summary": "Is the API ready to serve",
        "description": "Answers `200` once the API can reach its database, `503 not_ready` when it cannot.",
        "security": [],
        "responses": {
          "200": {
            "description": "Ready.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ready"
                      ]
                    }
                  }
                },
                "example": {
                  "status": "ready"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "503": [
            {
              "code": "not_ready",
              "description": "The database is unreachable."
            }
          ]
        }
      }
    },
    "/v1/streams": {
      "get": {
        "operationId": "listStreams",
        "tags": [
          "Streams"
        ],
        "summary": "List streams",
        "description": "Your streams, newest booked first. Page with `limit` and `before`:\npass the previous page's `next_before` as `before` until it is absent.\nThe ingest key is never in a list.\n",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "description": "Only streams in this state.",
            "schema": {
              "$ref": "#/components/schemas/StreamState"
            },
            "example": "live",
            "x-in-sample": true
          },
          {
            "name": "customer_ref",
            "in": "query",
            "description": "Only this customer's streams.",
            "schema": {
              "$ref": "#/components/schemas/CustomerRef"
            },
            "example": "grace-chapel",
            "x-in-sample": true
          },
          {
            "name": "recording_state",
            "in": "query",
            "description": "Only streams whose recording is in this state (as you would see it, so a ready recording past its 30 days is `expired`).",
            "schema": {
              "$ref": "#/components/schemas/RecordingState"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "before",
            "in": "query",
            "description": "The previous page's `next_before`. Streams booked before this time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of streams.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "streams"
                  ],
                  "properties": {
                    "streams": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Stream"
                      }
                    },
                    "next_before": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Present when there may be another page. Pass it as `before`."
                    }
                  }
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/StreamList"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_state",
              "description": "`state` is not one of the stream states."
            },
            {
              "code": "invalid_customer_ref",
              "description": "`customer_ref` is not a valid customer reference."
            },
            {
              "code": "invalid_recording_state",
              "description": "`recording_state` is not one of the recording states."
            },
            {
              "code": "invalid_limit",
              "description": "`limit` is not a positive whole number."
            },
            {
              "code": "invalid_before",
              "description": "`before` is not an RFC 3339 time."
            }
          ]
        }
      },
      "post": {
        "operationId": "createStream",
        "tags": [
          "Streams"
        ],
        "summary": "Book a stream",
        "description": "Books a broadcast, now or for later, and returns its ingest address.\n**The ingest key is returned once, here, and never again** (rotate it\nto get a new one).\n\nA booking with `scheduled_start_at` starts itself at that time, and\n**billing starts then whether or not your encoder is connected**. Leave\nit out and call [start](#startStream) when you are ready. An encoder\ncan connect up to an hour before a booked start to check the picture;\nnothing is billed or shown until the start.\n\nSend an `Idempotency-Key` so a retried request returns the first\nbooking instead of making a second.\n",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "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`.",
            "schema": {
              "type": "string",
              "maxLength": 255,
              "pattern": "^[!-~]*$"
            },
            "example": "6f1c9a2e-booking-0001",
            "x-in-sample": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateStream"
              },
              "example": {
                "customer_ref": "grace-chapel",
                "title": "Sunday Service",
                "tier": "standard_1080p",
                "scheduled_start_at": "2026-10-18T15:00:00Z"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The booked stream, with its ingest address and key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/StreamBooked"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_body",
              "description": "The body is not a JSON object of the documented fields."
            },
            {
              "code": "invalid_tier",
              "description": "`tier` is not `standard_1080p` or `pro_4k`."
            },
            {
              "code": "missing_field",
              "description": "`title` is missing."
            },
            {
              "code": "invalid_customer_ref",
              "description": "`customer_ref` is missing or not a valid customer reference."
            },
            {
              "code": "invalid_title",
              "description": "`title` is longer than 200 characters."
            },
            {
              "code": "invalid_scheduled_start",
              "description": "`scheduled_start_at` is more than 15 minutes in the past."
            },
            {
              "code": "invalid_interactions",
              "description": "`interactions` is not an object of the documented switches."
            },
            {
              "code": "invalid_viewer_access",
              "description": "`visibility` or `access_code` is not acceptable; `detail` says why."
            },
            {
              "code": "invalid_idempotency_key",
              "description": "`Idempotency-Key` is longer than 255 characters, or has spaces or characters outside printable ASCII."
            }
          ],
          "402": [
            {
              "code": "card_required",
              "description": "An organization that signed itself up has no card on file yet. Add one in the console (Billing)."
            },
            {
              "code": "payment_required",
              "description": "Streaming is paused: a bill is unpaid after its seven days. Pay it in the console (Billing); streaming resumes at once."
            }
          ],
          "409": [
            {
              "code": "already_active",
              "description": "This customer already has a stream on air and your organization limits that."
            },
            {
              "code": "customer_archived",
              "description": "The customer is archived. Unarchive it to book streams for it."
            }
          ],
          "422": [
            {
              "code": "idempotency_key_reused",
              "description": "This `Idempotency-Key` was used for a different booking."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "get": {
        "operationId": "getStream",
        "tags": [
          "Streams"
        ],
        "summary": "Read a stream",
        "description": "A stream's state, its legs (the three independent encodings) while it is\non air, whether an encoder is connected, and its recording's state. Poll\nit every few seconds while live; it contains no credential.\n",
        "responses": {
          "200": {
            "description": "The stream.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/StreamLive"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ]
        }
      },
      "patch": {
        "operationId": "updateStream",
        "tags": [
          "Streams"
        ],
        "summary": "Change who may watch, and the room",
        "description": "Makes a stream public or private, sets or regenerates its access code,\nswitches chat, reactions and highlights, and (when captions are enabled)\nchanges its caption settings. Allowed in any state; a new code applies\nto new viewers, not to URLs already handed out.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateStream"
              },
              "example": {
                "visibility": "private",
                "regenerate_access_code": true,
                "interactions": {
                  "chat": true,
                  "reactions": true,
                  "highlights": false
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stream as it now is. `viewer_access.access_code` is present only when this call set or generated one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_body",
              "description": "The body has a field that is not one of the documented ones."
            },
            {
              "code": "missing_field",
              "description": "None of the fields was given."
            },
            {
              "code": "invalid_interactions",
              "description": "`interactions` is not an object of the documented switches."
            },
            {
              "code": "invalid_viewer_access",
              "description": "`visibility` or `access_code` is not acceptable; `detail` says why."
            },
            {
              "code": "invalid_captions",
              "description": "`captions` is not acceptable; `detail` names the field."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ],
          "409": [
            {
              "code": "stream_ended",
              "description": "The stream is over; its captions can no longer change."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/start": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "post": {
        "operationId": "startStream",
        "tags": [
          "Streams"
        ],
        "summary": "Start a stream",
        "description": "Brings a booked stream up now: three legs start encoding and billing\nbegins. It goes live within about a minute of your encoder's video\narriving, while it gets ready. A stream left to start itself at its\nbooked time gets ready in the five minutes before, and goes live within\nseconds of the start.\nCalling it again on a stream already starting or live returns it\nunchanged.\n",
        "responses": {
          "200": {
            "description": "The stream, now `starting` (or already `starting` or `live`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "402": [
            {
              "code": "cap_exceeded",
              "description": "Your organization has reached the monthly stream-hour cap set on its account."
            },
            {
              "code": "payment_required",
              "description": "Streaming is paused: a bill is unpaid after its seven days. Pay it in the console (Billing); streaming resumes at once."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ],
          "409": [
            {
              "code": "already_active",
              "description": "This customer already has a stream on air and your organization limits that."
            },
            {
              "code": "stream_ended",
              "description": "The stream is over and cannot be started again. Book a new one."
            }
          ],
          "503": [
            {
              "code": "no_rate",
              "description": "No price is configured for this tier. Contact us."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/end": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "post": {
        "operationId": "endStream",
        "tags": [
          "Streams"
        ],
        "summary": "End a stream",
        "description": "Takes a stream down and stops billing. Its recording starts to be made.\nCalling it again returns the same answer and bills nothing more. A\nbooking that never started ends with `billed_usd: 0`.\n",
        "responses": {
          "200": {
            "description": "The ended stream and what it bills.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Stream"
                    },
                    {
                      "type": "object",
                      "required": [
                        "billed_usd"
                      ],
                      "properties": {
                        "billed_usd": {
                          "type": "number",
                          "description": "What this broadcast bills, in US dollars, rounded once to the cent."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/ingest-key": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "post": {
        "operationId": "rotateIngestKey",
        "tags": [
          "Streams"
        ],
        "summary": "Rotate the ingest key",
        "description": "Replaces a stream's ingest key and returns the new address. The old key\nstops working immediately. Use it if a key leaks, or to get a key for a\nstream booked by a retried request.\n",
        "responses": {
          "200": {
            "description": "The new ingest address and key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ingest"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization, or it is over."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/playback": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "get": {
        "operationId": "getPlayback",
        "tags": [
          "Playback"
        ],
        "summary": "Get a playback URL",
        "description": "A signed HLS URL for a player, valid for six hours: the live stream\nwhile it is on air, and the recording once it has ended.\n\nCall it from **your server** for each viewer session. For a private\nstream, pass `viewer=1` with the viewer's access code (and their IP\naddress in `X-Viewer-IP`, so wrong codes are limited per viewer rather\nthan against your server). A call without `viewer=1` is your own server\nand is trusted.\n\nWhen the live room is on, the response also carries a viewer pass for\nit (`room`), and, when captions are on, the caption tracks.\n",
        "parameters": [
          {
            "name": "viewer",
            "in": "query",
            "description": "`1` when this pass is for a viewer and should be checked against the stream's access code. Left out, the call is trusted as your own.",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          },
          {
            "name": "access_code",
            "in": "query",
            "description": "The viewer's access code for a private stream (or send `X-Access-Code`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "viewer_ip",
            "in": "query",
            "description": "The viewer's IP address (or send `X-Viewer-IP`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/ViewerName"
          },
          {
            "$ref": "#/components/parameters/ViewerMember"
          },
          {
            "$ref": "#/components/parameters/AccessCodeHeader"
          },
          {
            "$ref": "#/components/parameters/ViewerIPHeader"
          }
        ],
        "responses": {
          "200": {
            "description": "The playback URL, when it expires, and the room pass and caption tracks when they apply.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Playback"
                },
                "example": {
                  "playback_url": "https://media.cardinal.stream/1846bdac2d8c0ce0e846d4a3817012c9/master.m3u8?t=v1.eyJw...",
                  "expires_at": "2026-09-20T20:41:07Z"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_viewer",
              "description": "`viewer` is not `1` or `true`."
            },
            {
              "code": "invalid_viewer_name",
              "description": "`X-Viewer-Name` is not a 1-40 character name, percent-encoded."
            },
            {
              "code": "invalid_viewer_member",
              "description": "`X-Viewer-Member` is not `1`, or was sent without `X-Viewer-Name`."
            }
          ],
          "403": [
            {
              "code": "access_code_required",
              "description": "The stream is private and no access code was given."
            },
            {
              "code": "access_code_invalid",
              "description": "That access code is not this stream's."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ],
          "409": [
            {
              "code": "not_published_yet",
              "description": "No video has arrived for this stream yet."
            }
          ],
          "410": [
            {
              "code": "recording_deleted",
              "description": "The stream has ended and its recording was deleted."
            },
            {
              "code": "recording_expired",
              "description": "The stream has ended and its recording is past its 30 days."
            }
          ],
          "429": [
            {
              "code": "too_many_attempts",
              "description": "Too many wrong access codes from this viewer; try again after `Retry-After` seconds."
            }
          ],
          "503": [
            {
              "code": "busy",
              "description": "Too many viewers are being checked at once; retry after `Retry-After` seconds."
            },
            {
              "code": "playback_unconfigured",
              "description": "Playback is not available on this deployment."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/room": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "get": {
        "operationId": "getRoomPass",
        "tags": [
          "Live room"
        ],
        "summary": "Get a live room pass",
        "description": "A pass into the stream's live room (chat, reactions, highlights) for a\nviewer or a moderator: a WebSocket URL and a token, valid six hours for\na viewer and four for a moderator. A viewer can join only while the\nstream is live. Name a viewer you have signed in with `X-Viewer-Name`.\n",
        "parameters": [
          {
            "name": "role",
            "in": "query",
            "description": "`viewer` (the default) or `moderator`.",
            "schema": {
              "type": "string",
              "enum": [
                "viewer",
                "moderator"
              ]
            },
            "example": "moderator",
            "x-in-sample": true
          },
          {
            "name": "viewer",
            "in": "query",
            "description": "`1` to check a viewer's access code for a private stream, as for playback.",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          },
          {
            "name": "access_code",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The viewer's access code for a private stream (or send `X-Access-Code`)."
          },
          {
            "name": "viewer_ip",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The viewer's IP address (or send `X-Viewer-IP`)."
          },
          {
            "$ref": "#/components/parameters/ViewerName"
          },
          {
            "$ref": "#/components/parameters/ViewerMember"
          },
          {
            "$ref": "#/components/parameters/AccessCodeHeader"
          },
          {
            "$ref": "#/components/parameters/ViewerIPHeader"
          }
        ],
        "responses": {
          "200": {
            "description": "The pass.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomPassResponse"
                },
                "example": {
                  "url": "wss://live.cardinal.stream/rooms/1846bdac2d8c0ce0e846d4a3817012c9",
                  "token": "v1.eyJy...",
                  "expires_at": "2026-09-20T19:41:07Z",
                  "role": "moderator",
                  "viewer_id": "q2Vt3b0yJ1tF6pXhR8m4Ag"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_role",
              "description": "`role` is not `viewer` or `moderator`, or a viewer check asked for a moderator pass."
            },
            {
              "code": "invalid_viewer",
              "description": "`viewer` is not `1` or `true`."
            },
            {
              "code": "invalid_viewer_name",
              "description": "`X-Viewer-Name` is not a 1-40 character name, percent-encoded."
            },
            {
              "code": "invalid_viewer_member",
              "description": "`X-Viewer-Member` is not `1`, or was sent without `X-Viewer-Name`."
            }
          ],
          "403": [
            {
              "code": "access_code_required",
              "description": "The stream is private and no access code was given."
            },
            {
              "code": "access_code_invalid",
              "description": "That access code is not this stream's."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ],
          "409": [
            {
              "code": "not_live",
              "description": "A viewer can join only while the stream is live."
            }
          ],
          "429": [
            {
              "code": "too_many_attempts",
              "description": "Too many wrong access codes from this viewer."
            }
          ],
          "503": [
            {
              "code": "live_unconfigured",
              "description": "The live room is not available on this deployment."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/highlights": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "post": {
        "operationId": "postHighlight",
        "tags": [
          "Live room"
        ],
        "summary": "Show a gift in the room",
        "description": "Shows a gift from your own giving system to everyone in the room, while\nthe stream is live. Cardinal takes no payments: post a highlight after\nyour giving system has taken the gift. `id` is yours and makes the call\nsafe to retry: a repeat is answered `200` with `duplicate: true` and\nshown once. `message` is accepted and discarded -- it reaches no viewer.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Highlight"
              },
              "example": {
                "id": "gift-5f1c0a92",
                "kind": "gift",
                "amount_display": "$50",
                "name": "The Alvarez family"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A repeat of an `id` already shown; nothing new was shown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HighlightResult"
                }
              }
            }
          },
          "202": {
            "description": "Delivered to the room (`displayed` is `false` when gifts are on but their display is switched off).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HighlightResult"
                },
                "example": {
                  "delivered": 312,
                  "duplicate": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_body",
              "description": "The body has a field that is not one of the documented ones."
            },
            {
              "code": "unknown_kind",
              "description": "`kind` is not `gift`."
            },
            {
              "code": "invalid_id",
              "description": "`id` is not 1-80 letters, digits, `.`, `:`, `_` or `-`."
            },
            {
              "code": "invalid_amount_display",
              "description": "`amount_display` is not 1-24 characters."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ],
          "409": [
            {
              "code": "not_live",
              "description": "Highlights can be posted only while the stream is live."
            },
            {
              "code": "highlights_off",
              "description": "Highlights are switched off for this stream."
            }
          ],
          "502": [
            {
              "code": "room_unreachable",
              "description": "The live room did not answer; retry shortly."
            }
          ],
          "503": [
            {
              "code": "live_unconfigured",
              "description": "The live room is not available on this deployment."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/moderation": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "post": {
        "operationId": "moderate",
        "tags": [
          "Live room"
        ],
        "summary": "Moderate the room",
        "description": "Removes a chat message, or mutes or unmutes a viewer (optionally removing\neverything they have said). The ids come from the room's own messages.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Moderation"
              },
              "example": {
                "action": "mute_viewer",
                "viewer_id": "q2Vt3b0yJ1tF6pXhR8m4Ag",
                "remove_messages": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What the room did.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModerationResult"
                },
                "example": {
                  "muted": true,
                  "removed": 3
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_body",
              "description": "The body has a field that is not one of the documented ones."
            },
            {
              "code": "unknown_action",
              "description": "`action` is not `remove_message`, `mute_viewer` or `unmute_viewer`."
            },
            {
              "code": "invalid_message_id",
              "description": "`message_id` is missing for `remove_message`."
            },
            {
              "code": "invalid_viewer_id",
              "description": "`viewer_id` is missing for a mute or unmute."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ],
          "502": [
            {
              "code": "room_unreachable",
              "description": "The live room did not answer; retry shortly."
            }
          ],
          "503": [
            {
              "code": "live_unconfigured",
              "description": "The live room is not available on this deployment."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/recording": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "get": {
        "operationId": "getRecording",
        "tags": [
          "Recordings"
        ],
        "summary": "Read a recording",
        "description": "A stream's recording and, once it is `ready`, a link to download it.\nThe link is signed for this one file, minted fresh on every read, and\nlives an hour unless you ask for longer with `link_ttl` (up to 7 days,\nnever past the recording's deletion). Use a long link to hand the file\nto another system that imports it from a URL.\n\nDownloads are free, support `Range` requests for resuming, and run at\nyour connection's speed on a single connection.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/LinkTTL"
          }
        ],
        "responses": {
          "200": {
            "description": "The recording.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recording"
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/RecordingReady"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_link_ttl",
              "description": "`link_ttl` is not a whole number of seconds from 60 to 604800."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ]
        }
      },
      "delete": {
        "operationId": "deleteRecording",
        "tags": [
          "Recordings"
        ],
        "summary": "Delete a recording",
        "description": "Deletes the MP4 and every segment of the broadcast, for good, once you\nhave your copy. Only after the stream has ended. Answers `202` while the\ndeletion runs and `200` once it is done; calling it again is safe.\n",
        "responses": {
          "200": {
            "description": "Deleted (or there was nothing to delete).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recording"
                }
              }
            }
          },
          "202": {
            "description": "Deleting.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recording"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ],
          "409": [
            {
              "code": "stream_not_ended",
              "description": "End the stream before deleting its recording."
            }
          ]
        }
      }
    },
    "/v1/recordings": {
      "get": {
        "operationId": "listRecordings",
        "tags": [
          "Recordings"
        ],
        "summary": "List recordings",
        "description": "Every recording you have, most recently **ended** first, each ready one\nwith its own fresh download link -- the one call for moving every\ncustomer's recordings into your own storage. Page with `cursor` until\n`next_cursor` is absent. Only broadcasts that have a recording are listed.\n",
        "parameters": [
          {
            "name": "customer_ref",
            "in": "query",
            "description": "Only this customer's recordings.",
            "schema": {
              "$ref": "#/components/schemas/CustomerRef"
            },
            "example": "grace-chapel",
            "x-in-sample": true
          },
          {
            "$ref": "#/components/parameters/RecordingStateFilter"
          },
          {
            "$ref": "#/components/parameters/EndedAfter"
          },
          {
            "$ref": "#/components/parameters/EndedBefore"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/LinkTTL"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of recordings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingList"
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/RecordingList"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_customer_ref",
              "description": "`customer_ref` is not a valid customer reference."
            },
            {
              "code": "invalid_state",
              "description": "`state` is not one of the recording states (other than `none`)."
            },
            {
              "code": "invalid_ended_after",
              "description": "`ended_after` is not an RFC 3339 time."
            },
            {
              "code": "invalid_ended_before",
              "description": "`ended_before` is not an RFC 3339 time."
            },
            {
              "code": "invalid_limit",
              "description": "`limit` is not a positive whole number."
            },
            {
              "code": "invalid_cursor",
              "description": "`cursor` is not a `next_cursor` from a previous page."
            },
            {
              "code": "invalid_link_ttl",
              "description": "`link_ttl` is not a whole number of seconds from 60 to 604800."
            }
          ]
        }
      }
    },
    "/v1/customers": {
      "get": {
        "operationId": "listCustomers",
        "tags": [
          "Customers"
        ],
        "summary": "List customers",
        "description": "Your customers, ordered by `customer_ref`. Archived customers are left\nout unless you ask for them. Page with `cursor` until `next_cursor` is\nabsent.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "include_archived",
            "in": "query",
            "description": "`true` to include archived customers.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "1",
                "0"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of customers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "customers"
                  ],
                  "properties": {
                    "customers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Customer"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "description": "Present when there is another page. Pass it as `cursor`."
                    }
                  }
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/CustomerList"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_limit",
              "description": "`limit` is not a positive whole number."
            },
            {
              "code": "invalid_cursor",
              "description": "`cursor` is not a `next_cursor` from a previous page."
            },
            {
              "code": "invalid_include_archived",
              "description": "`include_archived` is not `true` or `false`."
            }
          ]
        }
      },
      "post": {
        "operationId": "createCustomer",
        "tags": [
          "Customers"
        ],
        "summary": "Create a customer",
        "description": "Registers a customer ahead of its first stream, with a name and your\nown metadata. Booking a stream registers its customer anyway; create one\nonly to name it first.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCustomer"
              },
              "example": {
                "customer_ref": "grace-chapel",
                "name": "Grace Chapel",
                "metadata": {
                  "crm_id": "C-1042"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/Customer"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_body",
              "description": "The body is not a JSON object of the documented fields."
            },
            {
              "code": "invalid_customer_ref",
              "description": "`customer_ref` is missing or not a valid customer reference."
            },
            {
              "code": "invalid_name",
              "description": "`name` is not 1-200 characters without leading or trailing spaces."
            },
            {
              "code": "invalid_metadata",
              "description": "`metadata` has too many keys, a key or value too long, or is over 4 KB."
            }
          ],
          "409": [
            {
              "code": "customer_exists",
              "description": "A customer with this `customer_ref` already exists (booking a stream registers one)."
            }
          ]
        }
      }
    },
    "/v1/customers/{customer_ref}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerRefPath"
        }
      ],
      "get": {
        "operationId": "getCustomer",
        "tags": [
          "Customers"
        ],
        "summary": "Read a customer",
        "responses": {
          "200": {
            "description": "The customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/Customer"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_customer_ref",
              "description": "The path's customer reference is not valid."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such customer in your organization."
            }
          ]
        }
      },
      "patch": {
        "operationId": "updateCustomer",
        "tags": [
          "Customers"
        ],
        "summary": "Update or archive a customer",
        "description": "Every field is optional: absent leaves it, `\"name\": null` clears the\nname, `metadata` replaces the whole set, and `\"archived\": true` archives\nthe customer. An archived customer keeps its history and recordings, but\nno stream can be booked for it until it is unarchived.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCustomer"
              },
              "example": {
                "name": "Grace Chapel Downtown",
                "archived": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The customer as it now is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_body",
              "description": "The body is not a JSON object of the documented fields."
            },
            {
              "code": "invalid_customer_ref",
              "description": "The path's customer reference is not valid."
            },
            {
              "code": "invalid_name",
              "description": "`name` is not a string of 1-200 characters, or null."
            },
            {
              "code": "invalid_metadata",
              "description": "`metadata` has too many keys, a key or value too long, or is over 4 KB."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such customer in your organization."
            }
          ]
        }
      }
    },
    "/v1/customers/{customer_ref}/recordings": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerRefPath"
        }
      ],
      "get": {
        "operationId": "listCustomerRecordings",
        "tags": [
          "Recordings"
        ],
        "summary": "List one customer's recordings",
        "description": "The same as [List recordings](#listRecordings) for one customer. A\ncustomer with no recordings, or one you never used, is an empty list.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/RecordingStateFilter"
          },
          {
            "$ref": "#/components/parameters/EndedAfter"
          },
          {
            "$ref": "#/components/parameters/EndedBefore"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/LinkTTL"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the customer's recordings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingList"
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/RecordingList"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_customer_ref",
              "description": "The path's customer reference is not valid."
            },
            {
              "code": "invalid_state",
              "description": "`state` is not one of the recording states (other than `none`)."
            },
            {
              "code": "invalid_ended_after",
              "description": "`ended_after` is not an RFC 3339 time."
            },
            {
              "code": "invalid_ended_before",
              "description": "`ended_before` is not an RFC 3339 time."
            },
            {
              "code": "invalid_limit",
              "description": "`limit` is not a positive whole number."
            },
            {
              "code": "invalid_cursor",
              "description": "`cursor` is not a `next_cursor` from a previous page."
            },
            {
              "code": "invalid_link_ttl",
              "description": "`link_ttl` is not a whole number of seconds from 60 to 604800."
            }
          ]
        }
      }
    },
    "/v1/customers/{customer_ref}/caption-settings": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerRefPath"
        }
      ],
      "get": {
        "operationId": "getCaptionSettings",
        "tags": [
          "Captions"
        ],
        "summary": "Read a customer's caption settings",
        "description": "The caption settings new streams for this customer start with. `is_default` is true when none have been saved.",
        "responses": {
          "200": {
            "description": "The settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaptionSettingsResponse"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_customer_ref",
              "description": "The path's customer reference is not valid."
            }
          ]
        }
      },
      "put": {
        "operationId": "putCaptionSettings",
        "tags": [
          "Captions"
        ],
        "summary": "Save a customer's caption settings",
        "description": "The whole document; a field left out takes its default. New streams for the customer start with these.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CaptionSettingsInput"
              },
              "example": {
                "spoken_language": "en",
                "profanity_masking": true,
                "vocabulary": [
                  "Grace Chapel",
                  "Pastor Okafor"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings as saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaptionSettingsResponse"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_customer_ref",
              "description": "The path's customer reference is not valid."
            },
            {
              "code": "invalid_body",
              "description": "The body is not JSON."
            },
            {
              "code": "invalid_captions",
              "description": "A setting is not acceptable; `detail` names the field."
            }
          ],
          "409": [
            {
              "code": "translations_unavailable",
              "description": "Translated captions are not available to your organization."
            }
          ],
          "413": [
            {
              "code": "body_too_large",
              "description": "The document is over 128 KB."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/captions": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "get": {
        "operationId": "getStreamCaptions",
        "tags": [
          "Captions"
        ],
        "summary": "Read a stream's captions status",
        "description": "Whether captions are being produced for a stream right now, and how far behind the video they are.",
        "responses": {
          "200": {
            "description": "The status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaptionStatus"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/destinations": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        }
      ],
      "get": {
        "operationId": "listDestinations",
        "tags": [
          "Restreaming"
        ],
        "summary": "List a stream's destinations",
        "description": "The services a stream is sent on to, and how each is doing. Stream keys are never returned, only their last characters.",
        "responses": {
          "200": {
            "description": "The destinations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "destinations",
                    "max_destinations"
                  ],
                  "properties": {
                    "destinations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Destination"
                      }
                    },
                    "max_destinations": {
                      "type": "integer",
                      "description": "How many destinations a stream may have."
                    }
                  }
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/DestinationList"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ]
        }
      },
      "post": {
        "operationId": "createDestination",
        "tags": [
          "Restreaming"
        ],
        "summary": "Add a destination",
        "description": "Sends the stream on to another service while it is live. Give the\nserver URL and stream key the service shows you. The key is stored\nencrypted, never returned, and deleted an hour after the broadcast ends.\nDestinations on private networks, or Cardinal's own ingest, are refused.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateDestination"
              },
              "example": {
                "name": "Our channel",
                "url": "rtmps://live.example.com:443/app",
                "stream_key": "abcd-efgh-ijkl-mnop",
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The destination.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Destination"
                },
                "examples": {
                  "default": {
                    "$ref": "#/components/examples/Destination"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_body",
              "description": "The body is not a JSON object."
            },
            {
              "code": "missing_field",
              "description": "`name`, `url` or `stream_key` is missing."
            },
            {
              "code": "invalid_name",
              "description": "The name is not 1-80 visible characters, or contains the stream key."
            },
            {
              "code": "invalid_url",
              "description": "The URL is not an `rtmps://` server address with an application path (plain `rtmp://` is refused: it would send the key unencrypted), carries credentials, a query string, spaces or control characters (typed or percent-encoded), or contains the stream key."
            },
            {
              "code": "invalid_stream_key",
              "description": "The stream key is empty, too long, or has characters a stream key cannot."
            },
            {
              "code": "destination_unresolvable",
              "description": "The server name could not be found."
            },
            {
              "code": "destination_not_allowed",
              "description": "That server address cannot be used."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such stream in your organization."
            }
          ],
          "409": [
            {
              "code": "stream_ended",
              "description": "The stream has ended."
            },
            {
              "code": "destination_limit",
              "description": "The stream already has the most destinations allowed."
            }
          ],
          "503": [
            {
              "code": "restream_unconfigured",
              "description": "Restreaming is not available on this deployment."
            },
            {
              "code": "restream_unpriced",
              "description": "Restreaming is not available for this broadcast."
            }
          ]
        }
      }
    },
    "/v1/streams/{playback_id}/destinations/{destination_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PlaybackID"
        },
        {
          "$ref": "#/components/parameters/DestinationID"
        }
      ],
      "patch": {
        "operationId": "updateDestination",
        "tags": [
          "Restreaming"
        ],
        "summary": "Pause, resume or rename a destination",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateDestination"
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The destination as it now is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Destination"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_body",
              "description": "The body is not a JSON object."
            },
            {
              "code": "missing_field",
              "description": "Neither `enabled` nor `name` was given."
            },
            {
              "code": "invalid_name",
              "description": "The name is not 1-80 visible characters, or contains the stream key."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such stream or destination in your organization."
            }
          ]
        }
      },
      "delete": {
        "operationId": "deleteDestination",
        "tags": [
          "Restreaming"
        ],
        "summary": "Remove a destination",
        "responses": {
          "204": {
            "description": "Removed."
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "404": [
            {
              "code": "not_found",
              "description": "No such stream or destination in your organization."
            }
          ]
        }
      }
    },
    "/v1/player": {
      "get": {
        "operationId": "getPlayerSettings",
        "tags": [
          "Player"
        ],
        "summary": "Read your player brand",
        "description": "Your brand on the hosted player (cardinal.stream), as stored and as viewers see it.",
        "responses": {
          "200": {
            "description": "The settings and the resolved brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerSettingsResponse"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "put": {
        "operationId": "putPlayerSettings",
        "tags": [
          "Player"
        ],
        "summary": "Set your player brand",
        "description": "Replaces your brand on the hosted player. A field left out or null is\nunset. In the console only an owner may change it.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlayerSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings and the resolved brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerSettingsResponse"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_settings",
              "description": "A field is malformed; the detail says which."
            }
          ],
          "403": [
            {
              "code": "owner_only",
              "description": "In the console, only an owner may change what viewers see."
            }
          ]
        }
      }
    },
    "/v1/customers/{customer_ref}/player": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerRefPath"
        }
      ],
      "get": {
        "operationId": "getCustomerPlayerSettings",
        "tags": [
          "Player"
        ],
        "summary": "Read one customer's player brand",
        "description": "A partner's per-customer brand, which overrides the default field by field.",
        "responses": {
          "200": {
            "description": "The customer's settings and the brand its viewers see.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerSettingsResponse"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "404": [
            {
              "code": "not_found",
              "description": "No such customer in your organization."
            }
          ]
        }
      },
      "put": {
        "operationId": "putCustomerPlayerSettings",
        "tags": [
          "Player"
        ],
        "summary": "Set one customer's player brand",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlayerSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The customer's settings and the brand its viewers see.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerSettingsResponse"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_settings",
              "description": "A field is malformed; the detail says which."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such customer in your organization."
            }
          ]
        }
      }
    },
    "/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "tags": [
          "Usage"
        ],
        "summary": "Read a month's usage",
        "description": "What was streamed in a month (UTC) and what it cost, broken down stream\nby stream. With `customer_ref`, one customer's -- what a partner bills\nthat customer from.\n",
        "parameters": [
          {
            "name": "month",
            "in": "query",
            "description": "`YYYY-MM`. Defaults to the current month (UTC).",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}-[0-9]{2}$"
            },
            "example": "2026-09",
            "x-in-sample": true
          },
          {
            "name": "customer_ref",
            "in": "query",
            "description": "Only this customer's usage.",
            "schema": {
              "$ref": "#/components/schemas/CustomerRef"
            },
            "example": "grace-chapel",
            "x-in-sample": true
          }
        ],
        "responses": {
          "200": {
            "description": "The month's usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_month",
              "description": "`month` is not `YYYY-MM`."
            },
            {
              "code": "invalid_customer_ref",
              "description": "`customer_ref` is not a valid customer reference."
            }
          ]
        }
      }
    },
    "/v1/usage/months": {
      "get": {
        "operationId": "getUsageMonths",
        "tags": [
          "Usage"
        ],
        "summary": "Read usage month by month",
        "description": "One entry per calendar month (UTC), oldest first, with zero months\nincluded: broadcasts, billed stream minutes by tier, every priced line\nand the total in cents -- the amounts the month's invoice uses. The\ncurrent month is the invoice so far (broadcasts that have ended).\n",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "First month, `YYYY-MM`. Defaults to eleven months before `to`.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}-[0-9]{2}$"
            },
            "example": "2026-04",
            "x-in-sample": true
          },
          {
            "name": "to",
            "in": "query",
            "description": "Last month, `YYYY-MM`, inclusive. Defaults to the current month. At most 36 months from `from`.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}-[0-9]{2}$"
            },
            "example": "2026-09",
            "x-in-sample": true
          },
          {
            "name": "customer_ref",
            "in": "query",
            "description": "Only this customer's usage.",
            "schema": {
              "$ref": "#/components/schemas/CustomerRef"
            },
            "example": "grace-chapel"
          }
        ],
        "responses": {
          "200": {
            "description": "Usage by month.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageMonths"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_month",
              "description": "`from` or `to` is not `YYYY-MM`."
            },
            {
              "code": "invalid_range",
              "description": "`from` is after `to`, or the range is longer than 36 months."
            },
            {
              "code": "invalid_customer_ref",
              "description": "`customer_ref` is not a valid customer reference."
            }
          ]
        }
      }
    },
    "/v1/usage/customers": {
      "get": {
        "operationId": "getUsageByCustomer",
        "tags": [
          "Usage"
        ],
        "summary": "Read a month's usage by customer",
        "description": "One month's usage per customer, highest spend first, with the name you\ngave each. Only customers that streamed that month appear.\n",
        "parameters": [
          {
            "name": "month",
            "in": "query",
            "description": "`YYYY-MM`. Defaults to the current month (UTC).",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}-[0-9]{2}$"
            },
            "example": "2026-09",
            "x-in-sample": true
          }
        ],
        "responses": {
          "200": {
            "description": "Usage by customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageByCustomer"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_month",
              "description": "`month` is not `YYYY-MM`."
            }
          ]
        }
      }
    },
    "/v1/billing": {
      "get": {
        "operationId": "getBilling",
        "tags": [
          "Billing"
        ],
        "summary": "Read your billing",
        "description": "Your last invoices, the card on file and your account credit. Works while streaming is paused for an unpaid bill.",
        "responses": {
          "200": {
            "description": "Billing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Billing"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "503": [
            {
              "code": "billing_unavailable",
              "description": "Billing is not available on this deployment."
            }
          ]
        }
      }
    },
    "/v1/billing/upcoming": {
      "get": {
        "operationId": "getUpcomingBill",
        "tags": [
          "Billing"
        ],
        "summary": "Read the bill so far",
        "description": "This month's invoice as it stands: usage so far priced as the invoice\nwill price it, the platform fee, anything carried, the credit drawn\nfirst, the estimated charge and its date, and what is still owed on\nearlier invoices.\n",
        "responses": {
          "200": {
            "description": "The bill so far.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpcomingBill"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "503": [
            {
              "code": "billing_unavailable",
              "description": "Billing is not available on this deployment."
            }
          ]
        }
      }
    },
    "/v1/billing/card": {
      "put": {
        "operationId": "setCard",
        "tags": [
          "Billing"
        ],
        "summary": "Put a card on file",
        "description": "Sets the card streaming is billed to (or the backup card, tried when the\nmain one is declined). A card number never reaches this API: the console\ncollects it in the browser straight to the payment processor, which\nreturns the `payment_method_id` you send here.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetCard"
              },
              "example": {
                "payment_method_id": "pm_8f3c2a91d0",
                "backup": false
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "The card is on file."
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "400": [
            {
              "code": "invalid_body",
              "description": "The body is not JSON."
            },
            {
              "code": "invalid_request",
              "description": "The payment method is missing, unknown, removed, not a card, or another customer's; or it is the organization's first card and was not stored in the last hour."
            }
          ],
          "503": [
            {
              "code": "billing_unavailable",
              "description": "Billing is not available on this deployment."
            }
          ]
        }
      }
    },
    "/v1/billing/invoices/{invoice_id}/pay": {
      "parameters": [
        {
          "name": "invoice_id",
          "in": "path",
          "required": true,
          "description": "The invoice's `id`.",
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "example": "0d6f2b1c-3e4a-4b8f-9c11-5a7d2e9f8b10"
        }
      ],
      "post": {
        "operationId": "payInvoice",
        "tags": [
          "Billing"
        ],
        "summary": "Pay an invoice now",
        "description": "Charges an unpaid invoice now, after any account credit. Paying the\nbill that paused streaming resumes it at once.\nA declined card is `402 payment_failed` and changes nothing else.\n",
        "responses": {
          "200": {
            "description": "Paid, or settled by credit, or carried into next month because what remained was under $0.50.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentOutcome"
                },
                "example": {
                  "status": "succeeded",
                  "charged_cents": 2750,
                  "credited_cents": 1500,
                  "suspended": false
                }
              }
            }
          },
          "402": {
            "description": "The card was declined.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentFailed"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-errors": {
          "402": [
            {
              "code": "payment_failed",
              "description": "The card was not charged; `error_class` says why and `detail` says what to do, in words fit to show a person."
            }
          ],
          "404": [
            {
              "code": "not_found",
              "description": "No such invoice in your organization."
            }
          ],
          "409": [
            {
              "code": "not_payable",
              "description": "The invoice is not owed."
            }
          ],
          "503": [
            {
              "code": "billing_unavailable",
              "description": "Billing is not available on this deployment."
            }
          ]
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from the console, `strm_...`."
      }
    },
    "parameters": {
      "PlaybackID": {
        "name": "playback_id",
        "in": "path",
        "required": true,
        "description": "The stream's `playback_id`.",
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9]{16,48}$"
        },
        "example": "1846bdac2d8c0ce0e846d4a3817012c9"
      },
      "CustomerRefPath": {
        "name": "customer_ref",
        "in": "path",
        "required": true,
        "description": "Your identifier for the customer.",
        "schema": {
          "$ref": "#/components/schemas/CustomerRef"
        },
        "example": "grace-chapel"
      },
      "DestinationID": {
        "name": "destination_id",
        "in": "path",
        "required": true,
        "description": "The destination's `id`.",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "3b4f9c2e-8d1a-4f6b-a2c7-9e5d1b0f4a83"
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Page size, 1-200. Default 50; more than 200 is treated as 200.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "description": "The previous page's `next_cursor`.",
        "schema": {
          "type": "string"
        }
      },
      "LinkTTL": {
        "name": "link_ttl",
        "in": "query",
        "description": "Seconds each download link lives, 60 to 604800 (7 days). Default 3600. A link never outlives the recording.",
        "schema": {
          "type": "integer",
          "minimum": 60,
          "maximum": 604800
        },
        "example": 86400,
        "x-in-sample": true
      },
      "RecordingStateFilter": {
        "name": "state",
        "in": "query",
        "description": "Only recordings in this state. `ready` for those with a file to collect.",
        "schema": {
          "type": "string",
          "enum": [
            "finalizing",
            "exporting",
            "ready",
            "failed",
            "deleting",
            "deleted",
            "expired"
          ]
        },
        "example": "ready",
        "x-in-sample": true
      },
      "EndedAfter": {
        "name": "ended_after",
        "in": "query",
        "description": "Only broadcasts that ended at or after this time (RFC 3339).",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "EndedBefore": {
        "name": "ended_before",
        "in": "query",
        "description": "Only broadcasts that ended before this time (RFC 3339).",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "ViewerName": {
        "name": "X-Viewer-Name",
        "in": "header",
        "description": "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.",
        "schema": {
          "type": "string"
        }
      },
      "ViewerMember": {
        "name": "X-Viewer-Member",
        "in": "header",
        "description": "`1` to mark the named viewer as a member of the customer's community. Only with `X-Viewer-Name`.",
        "schema": {
          "type": "string",
          "enum": [
            "1"
          ]
        }
      },
      "AccessCodeHeader": {
        "name": "X-Access-Code",
        "in": "header",
        "description": "The viewer's access code for a private stream.",
        "schema": {
          "type": "string"
        }
      },
      "ViewerIPHeader": {
        "name": "X-Viewer-IP",
        "in": "header",
        "description": "The viewer's IP address, so wrong access codes are limited per viewer.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "Error": {
        "description": "An error. `error` is a stable code to branch on; `detail` is for a person.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "error": "not_found"
            }
          }
        }
      }
    },
    "examples": {
      "StreamBooked": {
        "value": {
          "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
        }
      },
      "StreamLive": {
        "value": {
          "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
        }
      },
      "RecordingReady": {
        "value": {
          "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"
        }
      },
      "RecordingList": {
        "value": {
          "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"
        }
      },
      "Customer": {
        "value": {
          "customer_ref": "grace-chapel",
          "name": "Grace Chapel",
          "metadata": {
            "crm_id": "C-1042"
          },
          "created_at": "2026-09-01T14:02:11Z",
          "updated_at": "2026-09-12T09:40:00Z",
          "archived_at": null
        }
      },
      "Destination": {
        "value": {
          "id": "3b4f9c2e-8d1a-4f6b-a2c7-9e5d1b0f4a83",
          "name": "Our channel",
          "url": "rtmps://live.example.com:443/app",
          "stream_key_hint": "...mnop",
          "enabled": true,
          "status": "idle",
          "last_error": null,
          "status_updated_at": null,
          "created_at": "2026-10-18T14:40:00Z",
          "updated_at": "2026-10-18T14:40:00Z"
        }
      },
      "StreamList": {
        "value": {
          "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"
        }
      },
      "CustomerList": {
        "value": {
          "customers": [
            {
              "customer_ref": "grace-chapel",
              "name": "Grace Chapel",
              "metadata": {
                "crm_id": "C-1042"
              },
              "created_at": "2026-09-01T14:02:11Z",
              "updated_at": "2026-09-12T09:40:00Z",
              "archived_at": null
            }
          ]
        }
      },
      "DestinationList": {
        "value": {
          "destinations": [
            {
              "id": "3b4f9c2e-8d1a-4f6b-a2c7-9e5d1b0f4a83",
              "name": "Our channel",
              "url": "rtmps://live.example.com:443/app",
              "stream_key_hint": "...mnop",
              "enabled": true,
              "status": "idle",
              "last_error": null,
              "status_updated_at": null,
              "created_at": "2026-10-18T14:40:00Z",
              "updated_at": "2026-10-18T14:40:00Z"
            }
          ],
          "max_destinations": 5
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A stable code to branch on, such as `not_found`."
          },
          "detail": {
            "type": "string",
            "description": "What went wrong, for a person. Absent when the code says it all."
          }
        }
      },
      "CustomerRef": {
        "type": "string",
        "pattern": "^[A-Za-z0-9._-]{1,128}$",
        "description": "Your identifier for a customer. 1-128 letters, digits, `.`, `_` or `-`; not `.` or `..`."
      },
      "Tier": {
        "type": "string",
        "enum": [
          "standard_1080p",
          "pro_4k"
        ],
        "description": "`standard_1080p` ($5 per stream-hour) or `pro_4k` ($20 per stream-hour)."
      },
      "StreamState": {
        "type": "string",
        "enum": [
          "scheduled",
          "starting",
          "live",
          "ended",
          "failed"
        ]
      },
      "RecordingState": {
        "type": "string",
        "enum": [
          "none",
          "finalizing",
          "exporting",
          "ready",
          "failed",
          "deleting",
          "deleted",
          "expired"
        ]
      },
      "Interactions": {
        "type": "object",
        "description": "What a stream's live room offers.",
        "required": [
          "chat",
          "reactions",
          "highlights",
          "reaction_floats",
          "highlight_display"
        ],
        "properties": {
          "chat": {
            "type": "boolean"
          },
          "reactions": {
            "type": "boolean"
          },
          "highlights": {
            "type": "boolean",
            "description": "Gift highlights from your giving system."
          },
          "reaction_floats": {
            "type": "boolean",
            "description": "Whether reactions float up the player."
          },
          "highlight_display": {
            "type": "boolean",
            "description": "Whether highlights are shown in the player."
          }
        }
      },
      "InteractionsInput": {
        "type": "object",
        "additionalProperties": false,
        "description": "Switches to change; any left out stay as they are.",
        "properties": {
          "chat": {
            "type": "boolean"
          },
          "reactions": {
            "type": "boolean"
          },
          "highlights": {
            "type": "boolean"
          },
          "reaction_floats": {
            "type": "boolean"
          },
          "highlight_display": {
            "type": "boolean"
          }
        }
      },
      "ViewerAccess": {
        "type": "object",
        "required": [
          "visibility",
          "has_access_code",
          "interactions"
        ],
        "properties": {
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ]
          },
          "has_access_code": {
            "type": "boolean"
          },
          "interactions": {
            "$ref": "#/components/schemas/Interactions"
          },
          "access_code": {
            "type": "string",
            "description": "Present only in the response that set or generated the code. Never readable again."
          }
        }
      },
      "Ingest": {
        "type": "object",
        "description": "Where an encoder sends the stream: RTMPS (RTMP inside TLS) on port 443.\nIngest is encrypted only; plain RTMP is not accepted. The srt_ fields\nappear only where SRT is offered, and SRT is encrypted too: one\npassphrase per stream, shown with the key.\n",
        "required": [
          "endpoints",
          "fanout_required"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "The ingest key. Only when it was just created or rotated."
          },
          "server_url": {
            "type": "string",
            "description": "The RTMPS server, for an encoder's Server field."
          },
          "stream_key": {
            "type": "string",
            "description": "The stream key, for the encoder's Stream Key field. Only with `key`."
          },
          "srt_url": {
            "type": "string",
            "description": "Only where SRT is offered. The whole SRT address for one-field encoders: stream ID, passphrase and AES-256. Only with `key`."
          },
          "srt_server": {
            "type": "string",
            "description": "Only where SRT is offered. The SRT server, srt://host:port. Not a secret."
          },
          "srt_stream_id": {
            "type": "string",
            "description": "Only where SRT is offered. publish:live/<playback_id>: names the broadcast, carries no key. Not a secret."
          },
          "srt_passphrase": {
            "type": "string",
            "description": "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": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every RTMPS address the stream accepts."
          },
          "fanout_required": {
            "type": "boolean",
            "description": "Whether the encoder must push to every endpoint for full redundancy."
          }
        }
      },
      "Leg": {
        "type": "object",
        "description": "One of a stream's three independent encodings.",
        "required": [
          "rank",
          "state"
        ],
        "properties": {
          "rank": {
            "type": "integer"
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "unhealthy"
            ]
          },
          "ready_since": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RecordingSummary": {
        "type": "object",
        "required": [
          "state"
        ],
        "properties": {
          "state": {
            "$ref": "#/components/schemas/RecordingState"
          },
          "bytes": {
            "type": "integer",
            "format": "int64"
          },
          "duration_seconds": {
            "type": "number"
          },
          "ready_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CaptionSettings": {
        "type": "object",
        "required": [
          "spoken_language",
          "translation_languages",
          "translation_mode",
          "profanity_masking",
          "sung_lyrics",
          "vocabulary"
        ],
        "properties": {
          "spoken_language": {
            "type": "string",
            "description": "`auto`, or the language spoken: one of `ar de en es fr it ja ko pl pt ru tl uk vi zh`."
          },
          "translation_languages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Reserved for translated captions. Empty."
          },
          "translation_mode": {
            "type": "string",
            "enum": [
              "on_demand",
              "always"
            ]
          },
          "profanity_masking": {
            "type": "boolean"
          },
          "sung_lyrics": {
            "type": "string",
            "enum": [
              "music-symbol",
              "caption"
            ],
            "description": "Whether sung lyrics are captioned or shown as a music symbol."
          },
          "vocabulary": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Up to 100 names and terms the captions should spell right."
          },
          "retired_source_languages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CaptionSettingsResponse": {
        "type": "object",
        "required": [
          "customer_ref",
          "settings",
          "is_default",
          "translations_available",
          "choices"
        ],
        "properties": {
          "customer_ref": {
            "$ref": "#/components/schemas/CustomerRef"
          },
          "settings": {
            "$ref": "#/components/schemas/CaptionSettings"
          },
          "is_default": {
            "type": "boolean",
            "description": "True when nothing has been saved for this customer."
          },
          "translations_available": {
            "type": "boolean",
            "description": "True when translated captions are priced for the organization, so languages chosen now would be produced."
          },
          "choices": {
            "type": "object",
            "description": "The languages each field may name, for a form.",
            "required": [
              "spoken",
              "translation"
            ],
            "properties": {
              "spoken": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CaptionLanguageChoice"
                }
              },
              "translation": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CaptionLanguageChoice"
                }
              }
            }
          }
        }
      },
      "CaptionLanguageChoice": {
        "type": "object",
        "required": [
          "code",
          "name"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "The language code, e.g. es or zh-Hans."
          },
          "name": {
            "type": "string",
            "description": "The language's name in itself."
          },
          "beta": {
            "type": "boolean",
            "description": "True for a translation still in beta."
          }
        }
      },
      "CaptionStatus": {
        "type": "object",
        "required": [
          "status",
          "lag_seconds",
          "translations_available",
          "languages"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "waiting",
              "running",
              "delayed",
              "unavailable",
              "ended"
            ],
            "description": "`delayed` when captions are more than 15 seconds behind the video."
          },
          "lag_seconds": {
            "type": "number",
            "nullable": true
          },
          "source_language": {
            "type": "string"
          },
          "translation_mode": {
            "type": "string"
          },
          "translations_available": {
            "type": "boolean"
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "lang",
                "name",
                "kind",
                "producing"
              ],
              "properties": {
                "lang": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "source",
                    "translation"
                  ]
                },
                "beta": {
                  "type": "boolean"
                },
                "producing": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "CaptionSettingsInput": {
        "type": "object",
        "properties": {
          "spoken_language": {
            "type": "string"
          },
          "translation_languages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "translation_mode": {
            "type": "string",
            "enum": [
              "on_demand",
              "always"
            ]
          },
          "profanity_masking": {
            "type": "boolean"
          },
          "sung_lyrics": {
            "type": "string",
            "enum": [
              "music-symbol",
              "caption"
            ]
          },
          "vocabulary": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Stream": {
        "type": "object",
        "required": [
          "playback_id",
          "customer_ref",
          "title",
          "tier",
          "state",
          "leg_count",
          "legs_ready",
          "recording",
          "encoder_connected",
          "viewer_access",
          "end_reason",
          "start_failure"
        ],
        "properties": {
          "playback_id": {
            "type": "string",
            "description": "The stream's public id. Unguessable."
          },
          "customer_ref": {
            "$ref": "#/components/schemas/CustomerRef"
          },
          "title": {
            "type": "string"
          },
          "tier": {
            "$ref": "#/components/schemas/Tier"
          },
          "state": {
            "$ref": "#/components/schemas/StreamState"
          },
          "scheduled_start_at": {
            "type": "string",
            "format": "date-time",
            "description": "When a booking starts itself."
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "ended_at": {
            "type": "string",
            "format": "date-time"
          },
          "unit_rate_usd_per_hour": {
            "type": "number",
            "description": "The stream-hour rate, fixed when it started."
          },
          "restream_unit_rate_usd_per_hour": {
            "type": "number",
            "description": "The destination-hour rate, fixed when it started."
          },
          "leg_count": {
            "type": "integer",
            "description": "How many independent encodings the stream runs."
          },
          "legs_ready": {
            "type": "integer",
            "description": "How many are encoding now."
          },
          "legs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Leg"
            },
            "description": "Each encoding's state, while the stream is not over."
          },
          "recording": {
            "$ref": "#/components/schemas/RecordingSummary"
          },
          "ingest": {
            "$ref": "#/components/schemas/Ingest"
          },
          "encoder_connected": {
            "type": "boolean",
            "description": "Whether video is arriving from an encoder now."
          },
          "viewer_access": {
            "$ref": "#/components/schemas/ViewerAccess"
          },
          "end_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "customer_ended",
              "no_video",
              "max_duration",
              "failed",
              "not_started"
            ],
            "description": "Why the stream ended. Null until it has."
          },
          "start_failure": {
            "type": "string",
            "nullable": true,
            "enum": [
              "cap_exceeded",
              "already_on_air",
              "no_rate",
              "missed_window",
              "org_inactive",
              "payment_required"
            ],
            "description": "Why a booking could not start. Null unless it could not."
          },
          "captions": {
            "$ref": "#/components/schemas/CaptionSettings"
          }
        }
      },
      "CreateStream": {
        "type": "object",
        "required": [
          "customer_ref",
          "title",
          "tier"
        ],
        "properties": {
          "customer_ref": {
            "$ref": "#/components/schemas/CustomerRef"
          },
          "title": {
            "type": "string",
            "maxLength": 200,
            "description": "Up to 200 characters."
          },
          "tier": {
            "$ref": "#/components/schemas/Tier"
          },
          "scheduled_start_at": {
            "type": "string",
            "format": "date-time",
            "description": "Book it for later. Left out, start it with the start call."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ],
            "description": "Default `public`."
          },
          "access_code": {
            "type": "string",
            "description": "For a private stream: 6-32 letters, digits or hyphens. Left out, one is generated."
          },
          "interactions": {
            "$ref": "#/components/schemas/InteractionsInput"
          }
        }
      },
      "UpdateStream": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ]
          },
          "access_code": {
            "type": "string",
            "description": "6-32 letters, digits or hyphens."
          },
          "regenerate_access_code": {
            "type": "boolean",
            "description": "Generate a new access code."
          },
          "interactions": {
            "$ref": "#/components/schemas/InteractionsInput"
          },
          "captions": {
            "$ref": "#/components/schemas/CaptionSettingsInput"
          }
        }
      },
      "Playback": {
        "type": "object",
        "required": [
          "playback_url",
          "expires_at"
        ],
        "properties": {
          "playback_url": {
            "type": "string",
            "description": "The HLS URL a player loads. A credential; mint one per viewer session."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "room": {
            "$ref": "#/components/schemas/RoomPass"
          },
          "captions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CaptionTrack"
            }
          }
        }
      },
      "CaptionTrack": {
        "type": "object",
        "required": [
          "lang",
          "name",
          "kind"
        ],
        "properties": {
          "lang": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "source",
              "translation"
            ]
          },
          "beta": {
            "type": "boolean"
          }
        }
      },
      "RoomPass": {
        "type": "object",
        "required": [
          "url",
          "token",
          "expires_at"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "The room's WebSocket URL."
          },
          "token": {
            "type": "string",
            "description": "The pass. A credential."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RoomPassResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RoomPass"
          },
          {
            "type": "object",
            "required": [
              "role",
              "viewer_id"
            ],
            "properties": {
              "role": {
                "type": "string",
                "enum": [
                  "viewer",
                  "moderator"
                ]
              },
              "viewer_id": {
                "type": "string",
                "description": "This pass's viewer, as the room's messages name them."
              }
            }
          }
        ]
      },
      "Highlight": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "amount_display"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_.:-]{1,80}$",
            "description": "Your id for the gift. A repeat is shown once."
          },
          "kind": {
            "type": "string",
            "enum": [
              "gift"
            ]
          },
          "amount_display": {
            "type": "string",
            "maxLength": 24,
            "description": "Shown exactly as given, such as `$50`."
          },
          "name": {
            "type": "string",
            "description": "The giver, only if they chose to be shown."
          },
          "message": {
            "type": "string",
            "description": "Accepted and discarded."
          }
        }
      },
      "HighlightResult": {
        "type": "object",
        "required": [
          "delivered",
          "duplicate"
        ],
        "properties": {
          "delivered": {
            "type": "integer",
            "description": "How many connected viewers it reached."
          },
          "duplicate": {
            "type": "boolean"
          },
          "displayed": {
            "type": "boolean"
          }
        }
      },
      "Moderation": {
        "type": "object",
        "required": [
          "action"
        ],
        "additionalProperties": false,
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "remove_message",
              "mute_viewer",
              "unmute_viewer"
            ]
          },
          "message_id": {
            "type": "string",
            "description": "For `remove_message`."
          },
          "viewer_id": {
            "type": "string",
            "description": "For `mute_viewer` and `unmute_viewer`."
          },
          "remove_messages": {
            "type": "boolean",
            "description": "With `mute_viewer`, also remove what they have said."
          }
        }
      },
      "ModerationResult": {
        "type": "object",
        "properties": {
          "removed": {
            "description": "Whether the message was removed, or how many messages a mute removed.",
            "oneOf": [
              {
                "type": "boolean"
              },
              {
                "type": "integer"
              }
            ]
          },
          "muted": {
            "type": "boolean"
          },
          "unmuted": {
            "type": "boolean"
          },
          "duplicate": {
            "type": "boolean"
          },
          "repeated": {
            "type": "boolean"
          }
        }
      },
      "Recording": {
        "type": "object",
        "required": [
          "playback_id",
          "state",
          "bytes",
          "duration_seconds",
          "missing_seconds",
          "ready_at",
          "retained_until"
        ],
        "properties": {
          "playback_id": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "none",
              "finalizing",
              "exporting",
              "ready",
              "failed",
              "deleting",
              "deleted",
              "expired"
            ],
            "description": "`none`: nothing to collect yet. `finalizing`, `exporting`: being made\n(about a minute per hour of broadcast). `ready`: the file exists.\n`failed`: it could not be made. `deleting`, `deleted`: you deleted it.\n`expired`: its 30 days ran out.\n"
          },
          "bytes": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "duration_seconds": {
            "type": "number",
            "nullable": true
          },
          "missing_seconds": {
            "type": "number",
            "nullable": true,
            "description": "How much of the broadcast is not in the file; 0 when complete."
          },
          "ready_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "retained_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the file will be deleted if you do not delete it first."
          },
          "download_url": {
            "type": "string",
            "description": "Only when `ready`. A credential for this one file."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When `download_url` stops working."
          },
          "failure_code": {
            "type": "string",
            "description": "Only when `failed`: `no_media`, `unreadable_media` or `export_failed`."
          },
          "delete_requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RecordingListItem": {
        "allOf": [
          {
            "type": "object",
            "required": [
              "customer_ref",
              "title",
              "tier",
              "ended_at"
            ],
            "properties": {
              "customer_ref": {
                "$ref": "#/components/schemas/CustomerRef"
              },
              "title": {
                "type": "string"
              },
              "tier": {
                "$ref": "#/components/schemas/Tier"
              },
              "ended_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          {
            "$ref": "#/components/schemas/Recording"
          }
        ]
      },
      "RecordingList": {
        "type": "object",
        "required": [
          "recordings"
        ],
        "properties": {
          "recordings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecordingListItem"
            }
          },
          "next_cursor": {
            "type": "string",
            "description": "Present when there is another page. Pass it as `cursor`."
          }
        }
      },
      "Customer": {
        "type": "object",
        "required": [
          "customer_ref",
          "name",
          "metadata",
          "created_at",
          "updated_at",
          "archived_at"
        ],
        "properties": {
          "customer_ref": {
            "$ref": "#/components/schemas/CustomerRef"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Your own labels, returned as given."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "archived_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "CreateCustomer": {
        "type": "object",
        "required": [
          "customer_ref"
        ],
        "properties": {
          "customer_ref": {
            "$ref": "#/components/schemas/CustomerRef"
          },
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "1-200 characters."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            },
            "description": "Up to 50 keys of 1-40 characters, values up to 500 characters, 4 KB in all."
          }
        }
      },
      "UpdateCustomer": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "description": "A new name, or null to clear it."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Replaces the whole set."
          },
          "archived": {
            "type": "boolean"
          }
        }
      },
      "Destination": {
        "type": "object",
        "required": [
          "id",
          "name",
          "url",
          "stream_key_hint",
          "enabled",
          "status",
          "last_error",
          "status_updated_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "stream_key_hint": {
            "type": "string",
            "description": "The last characters of the stream key."
          },
          "enabled": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "idle",
              "connecting",
              "live",
              "failing"
            ]
          },
          "last_error": {
            "type": "string",
            "nullable": true
          },
          "status_updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateDestination": {
        "type": "object",
        "required": [
          "name",
          "url",
          "stream_key"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 80,
            "description": "Shown to everyone who can see the stream."
          },
          "url": {
            "type": "string",
            "description": "The service's server URL: `rtmps://` (plain `rtmp://` is refused), without the key."
          },
          "stream_key": {
            "type": "string",
            "maxLength": 512
          },
          "enabled": {
            "type": "boolean",
            "description": "Default true."
          }
        }
      },
      "UpdateDestination": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "name": {
            "type": "string",
            "maxLength": 80
          }
        }
      },
      "StreamUsage": {
        "type": "object",
        "required": [
          "playback_id",
          "customer_ref",
          "title",
          "tier",
          "billable_minutes",
          "unit_rate_usd_per_hour",
          "amount_usd",
          "occurred_at",
          "restream_billable_minutes",
          "restream_destinations",
          "restream_unit_rate_usd_per_hour",
          "restream_amount_usd",
          "restream_destination_minutes",
          "end_reason",
          "started_at"
        ],
        "properties": {
          "playback_id": {
            "type": "string"
          },
          "customer_ref": {
            "$ref": "#/components/schemas/CustomerRef"
          },
          "title": {
            "type": "string"
          },
          "tier": {
            "$ref": "#/components/schemas/Tier"
          },
          "billable_minutes": {
            "type": "integer"
          },
          "unit_rate_usd_per_hour": {
            "type": "number"
          },
          "amount_usd": {
            "type": "number"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "restream_billable_minutes": {
            "type": "integer"
          },
          "restream_destinations": {
            "type": "integer"
          },
          "restream_unit_rate_usd_per_hour": {
            "type": "number",
            "nullable": true
          },
          "restream_amount_usd": {
            "type": "number"
          },
          "restream_destination_minutes": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "end_reason": {
            "type": "string",
            "nullable": true
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "captions": {
            "$ref": "#/components/schemas/CaptionUsage"
          }
        }
      },
      "CaptionUsage": {
        "type": "object",
        "required": [
          "captions_billable_minutes",
          "captions_amount_usd",
          "translations",
          "translation_billable_minutes",
          "translation_amount_usd"
        ],
        "properties": {
          "captions_billable_minutes": {
            "type": "integer"
          },
          "captions_amount_usd": {
            "type": "number"
          },
          "translations": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "language",
                "name",
                "billable_minutes",
                "unit_rate_usd_per_hour",
                "amount_usd"
              ],
              "properties": {
                "language": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "billable_minutes": {
                  "type": "integer"
                },
                "unit_rate_usd_per_hour": {
                  "type": "number"
                },
                "amount_usd": {
                  "type": "number"
                }
              }
            }
          },
          "translation_billable_minutes": {
            "type": "integer"
          },
          "translation_amount_usd": {
            "type": "number"
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "month",
          "stream_hours_used",
          "restream_destination_hours_used",
          "stream_hour_cap",
          "stream_hours_remaining",
          "streams"
        ],
        "properties": {
          "month": {
            "type": "string",
            "description": "`YYYY-MM`."
          },
          "customer_ref": {
            "$ref": "#/components/schemas/CustomerRef"
          },
          "stream_hours_used": {
            "type": "number"
          },
          "restream_destination_hours_used": {
            "type": "number"
          },
          "captions_hours_used": {
            "type": "number"
          },
          "translation_language_hours_used": {
            "type": "number"
          },
          "stream_hour_cap": {
            "type": "number",
            "nullable": true,
            "description": "Your organization's monthly cap, or null for none."
          },
          "stream_hours_remaining": {
            "type": "number",
            "nullable": true
          },
          "streams": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StreamUsage"
            }
          }
        }
      },
      "InvoiceLine": {
        "type": "object",
        "required": [
          "product",
          "tier",
          "minutes",
          "cents"
        ],
        "properties": {
          "product": {
            "type": "string",
            "enum": [
              "stream",
              "restream",
              "captions",
              "translation"
            ]
          },
          "tier": {
            "type": "string"
          },
          "minutes": {
            "type": "integer"
          },
          "cents": {
            "type": "integer"
          }
        }
      },
      "Invoice": {
        "type": "object",
        "required": [
          "id",
          "period",
          "status",
          "usage_cents",
          "platform_fee_cents",
          "carried_in_cents",
          "total_cents",
          "credit_cents",
          "charged_cents",
          "refunded_cents",
          "line_items",
          "attempts",
          "next_attempt_at",
          "last_error_class",
          "last_error_message",
          "processor_transaction_id",
          "paid_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "period": {
            "type": "string",
            "description": "`YYYY-MM`, the month billed."
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "paid",
              "failed",
              "carried"
            ]
          },
          "usage_cents": {
            "type": "integer"
          },
          "platform_fee_cents": {
            "type": "integer"
          },
          "carried_in_cents": {
            "type": "integer"
          },
          "total_cents": {
            "type": "integer"
          },
          "credit_cents": {
            "type": "integer"
          },
          "charged_cents": {
            "type": "integer"
          },
          "refunded_cents": {
            "type": "integer"
          },
          "line_items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceLine"
            }
          },
          "attempts": {
            "type": "integer"
          },
          "next_attempt_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_error_class": {
            "type": "string",
            "nullable": true,
            "enum": [
              "card_expired",
              "insufficient_funds",
              "card_invalid",
              "card_declined",
              "no_payment_method",
              "gateway_timeout",
              "gateway_error"
            ],
            "description": "Why the last charge failed. Treat a value not listed here as `gateway_error`."
          },
          "last_error_message": {
            "type": "string",
            "nullable": true,
            "description": "The same reason in plain words, with what to do about it, fit to show a person."
          },
          "processor_transaction_id": {
            "type": "string",
            "nullable": true
          },
          "paid_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "UsageLine": {
        "type": "object",
        "required": [
          "product",
          "tier",
          "minutes",
          "cents"
        ],
        "properties": {
          "product": {
            "type": "string",
            "enum": [
              "stream",
              "restream",
              "captions",
              "translation"
            ]
          },
          "tier": {
            "type": "string",
            "enum": [
              "standard_1080p",
              "pro_4k"
            ]
          },
          "minutes": {
            "type": "integer",
            "description": "Billed minutes."
          },
          "cents": {
            "type": "integer",
            "description": "The amount in whole cents, exactly as invoiced."
          }
        }
      },
      "StreamMinutesByTier": {
        "type": "object",
        "description": "Billed stream minutes by tier; both tiers are always present.",
        "required": [
          "standard_1080p",
          "pro_4k"
        ],
        "properties": {
          "standard_1080p": {
            "type": "integer"
          },
          "pro_4k": {
            "type": "integer"
          }
        }
      },
      "UsageMonth": {
        "type": "object",
        "required": [
          "month",
          "streams",
          "stream_minutes",
          "lines",
          "usage_cents"
        ],
        "properties": {
          "month": {
            "type": "string",
            "description": "`YYYY-MM` (UTC)."
          },
          "streams": {
            "type": "integer",
            "description": "Broadcasts that ended (and were billed) in the month."
          },
          "stream_minutes": {
            "$ref": "#/components/schemas/StreamMinutesByTier"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageLine"
            }
          },
          "usage_cents": {
            "type": "integer"
          }
        }
      },
      "UsageMonths": {
        "type": "object",
        "required": [
          "from",
          "to",
          "months"
        ],
        "properties": {
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "customer_ref": {
            "type": "string",
            "description": "Echoed when the request named one."
          },
          "months": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageMonth"
            }
          }
        }
      },
      "CustomerUsage": {
        "type": "object",
        "required": [
          "customer_ref",
          "name",
          "streams",
          "stream_minutes",
          "lines",
          "usage_cents"
        ],
        "properties": {
          "customer_ref": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "streams": {
            "type": "integer"
          },
          "stream_minutes": {
            "$ref": "#/components/schemas/StreamMinutesByTier"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageLine"
            }
          },
          "usage_cents": {
            "type": "integer"
          }
        }
      },
      "UsageByCustomer": {
        "type": "object",
        "required": [
          "month",
          "customers"
        ],
        "properties": {
          "month": {
            "type": "string"
          },
          "customers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomerUsage"
            }
          }
        }
      },
      "UpcomingBill": {
        "type": "object",
        "required": [
          "period",
          "billed_by_card",
          "lines",
          "usage_cents",
          "platform_fee_cents",
          "carried_cents",
          "credits_applied_cents",
          "estimated_total_cents",
          "charge_on",
          "outstanding_cents",
          "outstanding_invoices",
          "card_last4",
          "streaming_paused"
        ],
        "properties": {
          "period": {
            "type": "string",
            "description": "The current month, `YYYY-MM`."
          },
          "billed_by_card": {
            "type": "boolean"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageLine"
            }
          },
          "usage_cents": {
            "type": "integer"
          },
          "platform_fee_cents": {
            "type": "integer"
          },
          "carried_cents": {
            "type": "integer"
          },
          "credits_applied_cents": {
            "type": "integer"
          },
          "estimated_total_cents": {
            "type": "integer"
          },
          "charge_on": {
            "type": "string",
            "format": "date"
          },
          "outstanding_cents": {
            "type": "integer"
          },
          "outstanding_invoices": {
            "type": "integer"
          },
          "card_last4": {
            "type": "string",
            "nullable": true
          },
          "streaming_paused": {
            "type": "boolean",
            "description": "A bill is unpaid after its seven days: no stream can be booked or started until it is paid."
          }
        }
      },
      "PlayerSettings": {
        "type": "object",
        "description": "Every field is optional; null or absent is unset (inherits).",
        "properties": {
          "display_name": {
            "type": "string",
            "nullable": true,
            "maxLength": 100
          },
          "logo_url": {
            "type": "string",
            "nullable": true,
            "description": "An https:// image address."
          },
          "accent_color": {
            "type": "string",
            "nullable": true,
            "pattern": "^#[0-9a-fA-F]{6}$"
          },
          "theme": {
            "type": "string",
            "nullable": true,
            "enum": [
              "dark",
              "light"
            ]
          },
          "giving_url": {
            "type": "string",
            "nullable": true,
            "description": "Your giving page, https://. Unset: no giving button."
          },
          "giving_label": {
            "type": "string",
            "nullable": true,
            "maxLength": 30
          },
          "giving_on": {
            "type": "boolean",
            "nullable": true,
            "description": "false hides the giving button even where a page is set."
          },
          "show_badge": {
            "type": "boolean",
            "nullable": true,
            "description": "false removes \"Streaming by Cardinal\"."
          }
        }
      },
      "PlayerBrand": {
        "type": "object",
        "required": [
          "name",
          "logo_url",
          "accent_color",
          "theme",
          "show_badge",
          "giving"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "logo_url": {
            "type": "string",
            "nullable": true
          },
          "accent_color": {
            "type": "string"
          },
          "theme": {
            "type": "string",
            "enum": [
              "dark",
              "light"
            ]
          },
          "show_badge": {
            "type": "boolean"
          },
          "giving": {
            "type": "object",
            "nullable": true,
            "required": [
              "url",
              "label"
            ],
            "properties": {
              "url": {
                "type": "string"
              },
              "label": {
                "type": "string"
              }
            }
          }
        }
      },
      "PlayerSettingsResponse": {
        "type": "object",
        "required": [
          "settings",
          "brand"
        ],
        "properties": {
          "settings": {
            "$ref": "#/components/schemas/PlayerSettings"
          },
          "brand": {
            "$ref": "#/components/schemas/PlayerBrand"
          }
        }
      },
      "Billing": {
        "type": "object",
        "required": [
          "billed_by_card",
          "invoices",
          "credit_balance_cents",
          "streaming_paused"
        ],
        "properties": {
          "billed_by_card": {
            "type": "boolean",
            "description": "False when your organization is invoiced by agreement."
          },
          "invoices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Invoice"
            }
          },
          "credit_balance_cents": {
            "type": "integer"
          },
          "streaming_paused": {
            "type": "boolean",
            "description": "A bill is unpaid after its seven days: no stream can be booked or started until it is paid."
          },
          "card": {
            "type": "object",
            "required": [
              "brand",
              "last4",
              "exp_month",
              "exp_year",
              "has_backup"
            ],
            "properties": {
              "brand": {
                "type": "string",
                "nullable": true
              },
              "last4": {
                "type": "string",
                "nullable": true
              },
              "exp_month": {
                "type": "integer",
                "nullable": true
              },
              "exp_year": {
                "type": "integer",
                "nullable": true
              },
              "has_backup": {
                "type": "boolean"
              }
            }
          },
          "billing_email": {
            "type": "string",
            "nullable": true
          },
          "platform_fee_cents": {
            "type": "integer"
          },
          "processor_customer_id": {
            "type": "string",
            "nullable": true,
            "description": "Your customer record at the payment processor, set by your first card.\nTokenize any later card onto this record (pass it as `customer_id` to\nthe card form); a card stored on another record is refused.\n"
          }
        }
      },
      "SetCard": {
        "type": "object",
        "required": [
          "payment_method_id"
        ],
        "properties": {
          "payment_method_id": {
            "type": "string",
            "maxLength": 64
          },
          "backup": {
            "type": "boolean",
            "description": "Set the backup card instead of the main one."
          }
        }
      },
      "PaymentOutcome": {
        "type": "object",
        "required": [
          "status",
          "suspended",
          "charged_cents",
          "credited_cents"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "succeeded",
              "settled_without_charge",
              "carried",
              "failed"
            ]
          },
          "error_class": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "suspended": {
            "type": "boolean",
            "description": "Whether this attempt paused streaming. A payment never does."
          },
          "charged_cents": {
            "type": "integer"
          },
          "credited_cents": {
            "type": "integer"
          }
        }
      },
      "PaymentFailed": {
        "type": "object",
        "required": [
          "error",
          "error_class"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "payment_failed"
            ]
          },
          "detail": {
            "type": "string"
          },
          "error_class": {
            "type": "string",
            "enum": [
              "card_expired",
              "insufficient_funds",
              "card_invalid",
              "card_declined",
              "gateway_timeout",
              "gateway_error",
              "no_payment_method"
            ]
          }
        }
      }
    }
  }
}