{
  "openapi": "3.1.0",
  "info": {
    "title": "JustOnAir API",
    "version": "1.0.0",
    "summary": "Live video for your app or event: stream in, player out.",
    "description": "Create a stream, send video to it over RTMP or WHIP, and viewers watch through a signed HLS URL or the hosted player. Docs: https://docs.justonair.com. Everything as Markdown: https://docs.justonair.com/llms-full.txt",
    "contact": {
      "name": "JustOnAir",
      "url": "https://justonair.com"
    }
  },
  "servers": [
    {
      "url": "https://api.justonair.com"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Streams",
      "description": "Create, read, change and end streams."
    },
    {
      "name": "Playback",
      "description": "Signed URLs for your own player."
    },
    {
      "name": "Recordings",
      "description": "The MP4 made after a stream ends."
    },
    {
      "name": "Account",
      "description": "Balance and usage."
    },
    {
      "name": "Webhooks",
      "description": "Endpoints that get stream and recording events, and their delivery log."
    },
    {
      "name": "Embed",
      "description": "The hosted player’s public read."
    }
  ],
  "paths": {
    "/v1/streams": {
      "post": {
        "tags": [
          "Streams"
        ],
        "operationId": "createStream",
        "summary": "Create a stream",
        "description": "Reserves ingest capacity for 30 minutes and returns everything needed to go live: `rtmp_url` + `stream_key`, `whip_url`, `playback_url` and `embed_url`. The secrets are returned only here. Send an `Idempotency-Key` so a retry never creates a second stream.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            },
            "description": "Any unique string, e.g. a UUID. A retry with the same key and body returns the first response (header `Idempotent-Replayed: true`) for 24 h."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateStreamRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Store `stream_key` and `whip_url` now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedStream"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Credit below the 1.00 USD minimum (`insufficient_credit`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Account pending approval or suspended (`account_pending`, `tenant_suspended`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "This Idempotency-Key was used with a different body (`idempotency_key_reused`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "An account limit: `limit_pending_streams`, `limit_creation_rate`, `limit_ingest_hours`, `limit_resolution`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "No ingest capacity right now (`no_capacity`). Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Streams"
        ],
        "operationId": "listStreams",
        "summary": "List streams",
        "description": "Newest first.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Id of the last stream on the previous page."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "live",
                "ended",
                "expired",
                "cancelled"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of streams.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StreamList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Account pending approval or suspended (`account_pending`, `tenant_suspended`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/streams/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "description": "Stream id."
        }
      ],
      "get": {
        "tags": [
          "Streams"
        ],
        "operationId": "getStream",
        "summary": "Get a stream",
        "description": "Status, what is arriving (`ingest`), the recording, and `cost` so far. Poll it to follow a stream; every 3–5 s is fine.",
        "responses": {
          "200": {
            "description": "The stream.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Account pending approval or suspended (`account_pending`, `tenant_suspended`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such stream in your account (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Streams"
        ],
        "operationId": "updateStream",
        "summary": "Update a stream",
        "description": "Rename it, replace its metadata, or change player settings. Works in any status.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateStreamRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stream.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Account pending approval or suspended (`account_pending`, `tenant_suspended`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such stream in your account (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Streams"
        ],
        "operationId": "endStream",
        "summary": "End or cancel a stream",
        "description": "Live: ends it and disconnects the publisher within seconds. Pending: cancels it and releases the slot. Already finished: returns it unchanged.",
        "responses": {
          "200": {
            "description": "The stream, now `ended` or `cancelled`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Account pending approval or suspended (`account_pending`, `tenant_suspended`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such stream in your account (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/streams/{id}/stream-key": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "description": "Stream id."
        }
      ],
      "post": {
        "tags": [
          "Streams"
        ],
        "operationId": "replaceStreamKey",
        "summary": "Replace a lost stream key",
        "description": "Only while pending. The old key and WHIP URL stop working at once.",
        "responses": {
          "200": {
            "description": "The stream with new `stream_key` and `whip_url`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedStream"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such stream in your account (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Live or finished (`stream_not_pending`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/streams/{id}/playback-token": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "description": "Stream id."
        }
      ],
      "post": {
        "tags": [
          "Playback"
        ],
        "operationId": "createPlaybackToken",
        "summary": "Sign a playback URL",
        "description": "A fresh signed HLS URL, for example a short one per viewer minted by your backend. Refused once the stream is finished.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expires_in": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 604800,
                    "description": "Seconds. Default: until the stream could no longer be live, plus an hour."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybackToken"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such stream in your account (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The stream has ended (`stream_ended`). There is no replay.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/streams/{id}/recording": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "description": "Stream id."
        }
      ],
      "delete": {
        "tags": [
          "Recordings"
        ],
        "operationId": "deleteRecording",
        "summary": "Delete the recording",
        "description": "Deletes the MP4 now, or marks it to be deleted as soon as it exists.",
        "responses": {
          "200": {
            "description": "The stream; `recording.status` is `deleted` or `deleting`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such stream in your account (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/streams/{id}/recording/download": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "description": "Stream id."
        }
      ],
      "post": {
        "tags": [
          "Recordings"
        ],
        "operationId": "downloadRecording",
        "summary": "Get a download link",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expires_in": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 604800,
                    "default": 3600
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A signed link to the MP4.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingDownload"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such stream in your account (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not ready (`recording_not_ready`, with `details.status`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/streams/{id}/viewers": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "description": "Stream id."
        }
      ],
      "get": {
        "tags": [
          "Streams"
        ],
        "operationId": "getViewerSeries",
        "summary": "Viewers per minute",
        "description": "The viewer curve: live estimates from the hosted player, and exact counts from the CDN logs about an hour later.",
        "responses": {
          "200": {
            "description": "Per-minute counts from both sources.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ViewerSeries"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such stream in your account (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listWebhookEndpoints",
        "summary": "List webhook endpoints",
        "description": "Without secrets. Also returns `event_types`, every type an endpoint can subscribe to.",
        "responses": {
          "200": {
            "description": "Your endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointList"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "createWebhookEndpoint",
        "summary": "Add a webhook endpoint",
        "description": "Up to 5 per account. The answer includes `secret`; see [Webhooks](/webhooks) for verifying signatures.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "https URL on the public internet, up to 2000 characters."
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "stream.live",
                        "stream.ended",
                        "stream.expired",
                        "stream.cancelled",
                        "recording.ready",
                        "recording.failed",
                        "recording.deleted"
                      ]
                    },
                    "description": "Leave out for all events, including ones added later."
                  },
                  "description": {
                    "type": "string",
                    "description": "Up to 200 characters."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The endpoint, with its secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "5 endpoints already (`limit_webhook_endpoints`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Endpoint id (`whk_…`)."
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "getWebhookEndpoint",
        "summary": "Get a webhook endpoint",
        "description": "With its secret.",
        "responses": {
          "200": {
            "description": "The endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "updateWebhookEndpoint",
        "summary": "Change a webhook endpoint",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "description": ""
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "stream.live",
                        "stream.ended",
                        "stream.expired",
                        "stream.cancelled",
                        "recording.ready",
                        "recording.failed",
                        "recording.deleted"
                      ]
                    }
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": ""
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "False stops sending; queued deliveries fail."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "deleteWebhookEndpoint",
        "summary": "Delete a webhook endpoint",
        "description": "Its delivery log goes with it.",
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/secret": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Endpoint id (`whk_…`)."
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "rotateWebhookSecret",
        "summary": "Rotate the signing secret",
        "description": "The old secret keeps signing alongside the new one for 24 hours.",
        "responses": {
          "200": {
            "description": "The endpoint with the new secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/test": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Endpoint id (`whk_…`)."
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "testWebhookEndpoint",
        "summary": "Send a test event",
        "description": "Queues a `webhook.test`; it is sent within a couple of seconds.",
        "responses": {
          "202": {
            "description": "The queued delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/deliveries": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Endpoint id (`whk_…`)."
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listWebhookDeliveries",
        "summary": "Delivery log",
        "description": "Newest first, kept 30 days.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "succeeded",
                "failed"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deliveries, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryList"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/deliveries/{delivery_id}/retry": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Endpoint id (`whk_…`)."
        },
        {
          "name": "delivery_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "`dlv_…`"
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "retryWebhookDelivery",
        "summary": "Send a delivery again",
        "description": "A failed delivery gets one more attempt now; a pending one is brought forward.",
        "responses": {
          "202": {
            "description": "The delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No pending or failed delivery with that id (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getUsage",
        "summary": "Balance and usage",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90,
              "default": 7
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Balance, rates and usage per UTC day.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/embed/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "description": "Stream id."
        }
      ],
      "get": {
        "tags": [
          "Embed"
        ],
        "operationId": "getEmbed",
        "summary": "Public stream status (hosted player)",
        "description": "No key needed; any web page may call it. Cached for a few seconds; the same read is served, cached at the CDN edge, at `https://play.joacdn.com/api/embed/{id}`. Rate limited per IP on unknown ids (60/min).",
        "security": [],
        "responses": {
          "200": {
            "description": "Status and, while live, a signed URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Embed"
                }
              }
            }
          },
          "404": {
            "description": "Unknown stream.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests (`rate_limited`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/embed/{id}/beat": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "description": "Stream id."
        }
      ],
      "post": {
        "tags": [
          "Embed"
        ],
        "operationId": "viewerBeat",
        "summary": "Report a viewer (sampled)",
        "description": "Sent by the hosted player, or by your own player to be counted live. Draw `u` once per page load; report every `beacon.interval_seconds` while `u` is below `beacon.rate` from the embed read. No body, so `navigator.sendBeacon(url)` works from any page.",
        "security": [],
        "parameters": [
          {
            "name": "s",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{12,32}$"
            },
            "description": "Random id for this page load."
          },
          {
            "name": "st",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "w",
                "p",
                "z",
                "b"
              ]
            },
            "description": "Waiting, playing, paused or buffering."
          },
          {
            "name": "u",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": 0,
              "exclusiveMaximum": 1
            },
            "description": "The random draw, 0 ≤ u < 1."
          }
        ],
        "responses": {
          "204": {
            "description": "Counted (or the stream is over and there is nothing to count)."
          },
          "400": {
            "description": "Invalid request (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown stream.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests (`rate_limited`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "stream.live": {
      "post": {
        "summary": "stream.live",
        "description": "See [Webhooks](/webhooks). Answer with any 2xx within 10 s.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "stream.ended": {
      "post": {
        "summary": "stream.ended",
        "description": "See [Webhooks](/webhooks). Answer with any 2xx within 10 s.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "stream.expired": {
      "post": {
        "summary": "stream.expired",
        "description": "See [Webhooks](/webhooks). Answer with any 2xx within 10 s.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "stream.cancelled": {
      "post": {
        "summary": "stream.cancelled",
        "description": "See [Webhooks](/webhooks). Answer with any 2xx within 10 s.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "recording.ready": {
      "post": {
        "summary": "recording.ready",
        "description": "See [Webhooks](/webhooks). Answer with any 2xx within 10 s.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "recording.failed": {
      "post": {
        "summary": "recording.failed",
        "description": "See [Webhooks](/webhooks). Answer with any 2xx within 10 s.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "recording.deleted": {
      "post": {
        "summary": "recording.deleted",
        "description": "See [Webhooks](/webhooks). Answer with any 2xx within 10 s.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "webhook.test": {
      "post": {
        "summary": "webhook.test",
        "description": "See [Webhooks](/webhooks). Answer with any 2xx within 10 s.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Authorization: Bearer joa_live_… — create keys in the dashboard under API keys."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, machine-readable. See the Errors page.",
                "examples": [
                  "limit_pending_streams"
                ]
              },
              "message": {
                "type": "string",
                "description": "For people. May change."
              },
              "details": {
                "type": "object",
                "description": "Extra facts, e.g. `limit` and `current` on a 429."
              }
            }
          }
        }
      },
      "PlayerSettings": {
        "type": "object",
        "required": [
          "show_name"
        ],
        "additionalProperties": false,
        "properties": {
          "show_name": {
            "type": "boolean",
            "description": "Show the stream name to viewers on the hosted player. Default true. False keeps the name out of the public embed read entirely."
          }
        }
      },
      "Ingest": {
        "type": [
          "object",
          "null"
        ],
        "description": "What the ingest node last measured arriving. Null until the first measurement.",
        "properties": {
          "width": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Frame width in pixels."
          },
          "height": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Frame height in pixels."
          },
          "kbps": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Bitrate of everything received (video, audio, overhead), averaged over about 30 s."
          },
          "observed_at": {
            "type": "string",
            "description": "When this was measured.",
            "format": "date-time"
          },
          "limit_warning": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set while a limit is broken: `limit_bitrate` or `limit_resolution`. Still broken 20 s later, the stream ends with that `end_reason`."
          },
          "keyframe_interval_seconds": {
            "type": [
              "number",
              "null"
            ],
            "description": "Average seconds between the publisher’s keyframes. Null for browser (WHIP) publishers."
          },
          "keyframe_warning": {
            "type": "boolean",
            "description": "True above 2.5 s. Set your encoder to a 2 s keyframe interval. A warning only; never ends a stream."
          }
        }
      },
      "Recording": {
        "type": [
          "object",
          "null"
        ],
        "description": "The recording: one MP4 of the top quality, made after the stream ends. Null while pending.",
        "properties": {
          "status": {
            "type": "string",
            "description": "`recording` while live, then `processing`, `ready`, `deleting`, `deleted`, `failed`, `not_recorded` (record: false) or `none` (never went live).",
            "enum": [
              "recording",
              "processing",
              "ready",
              "deleting",
              "deleted",
              "failed",
              "not_recorded",
              "none",
              "legacy"
            ]
          },
          "bytes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "MP4 size."
          },
          "duration_seconds": {
            "type": [
              "number",
              "null"
            ],
            "description": "MP4 length."
          },
          "retention_days": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Days kept after it is ready. Null: until you delete it."
          },
          "ready_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the MP4 became downloadable.",
            "format": "date-time"
          },
          "delete_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it will be deleted: the retention date, or 7 days after the balance reached zero, whichever is earlier.",
            "format": "date-time"
          },
          "delete_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "`retention` or `zero_balance`."
          },
          "deleted_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it was deleted.",
            "format": "date-time"
          }
        }
      },
      "Cost": {
        "type": "object",
        "description": "What this stream has cost so far at current rates. Only on `GET /v1/streams/{id}`. Amounts are decimal strings.",
        "properties": {
          "total_usd": {
            "type": "string",
            "description": "Everything below.",
            "pattern": "^-?\\d+\\.\\d+$",
            "examples": [
              "0.016790"
            ]
          },
          "delivery_bytes": {
            "type": "integer",
            "description": "Bytes delivered to viewers, counted up to `delivery_counted_until`."
          },
          "delivery_usd": {
            "type": "string",
            "description": "Delivery cost.",
            "pattern": "^-?\\d+\\.\\d+$",
            "examples": [
              "0.016790"
            ]
          },
          "delivery_counted_until": {
            "type": [
              "string",
              "null"
            ],
            "description": "CDN logs are read an hour at a time, so delivery trails reality by up to about 80 minutes.",
            "format": "date-time"
          },
          "ingest_seconds": {
            "type": "integer",
            "description": "Seconds of publishing."
          },
          "ingest_usd": {
            "type": "string",
            "description": "Encoding (abr_basic) or passthrough cost.",
            "pattern": "^-?\\d+\\.\\d+$",
            "examples": [
              "0.016790"
            ]
          },
          "storage_gb_hours": {
            "type": "number",
            "description": "Recording storage used."
          },
          "storage_usd": {
            "type": "string",
            "description": "Recording storage cost.",
            "pattern": "^-?\\d+\\.\\d+$",
            "examples": [
              "0.016790"
            ]
          }
        }
      },
      "Viewers": {
        "type": "object",
        "description": "Viewer counts. While the stream runs, `now` is estimated from the hosted player: exact up to about 400 viewers, within about 5% above. After the fact, the CDN logs give the exact count for every player (hosted, your own, apps), about an hour later; `peak` and `watch_minutes` use them once they exist. Only on `GET /v1/streams/{id}`.",
        "properties": {
          "now": {
            "type": [
              "object",
              "null"
            ],
            "description": "Live estimate from the hosted player. Null when nobody is on the player or the stream is over.",
            "properties": {
              "watching": {
                "type": "integer",
                "description": "Viewers with the video on screen (playing, paused or buffering)."
              },
              "waiting": {
                "type": "integer",
                "description": "Viewers on the waiting screen before the stream starts."
              },
              "buffering_pct": {
                "type": "number",
                "description": "Share of those watching whose video is stalled right now."
              },
              "measured_at": {
                "type": "string",
                "description": "",
                "format": "date-time"
              },
              "exact": {
                "type": "boolean",
                "description": "True when every viewer reports (small audiences), false when sampled."
              }
            }
          },
          "peak": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Most viewers in any one minute."
          },
          "peak_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "The start of that minute.",
            "format": "date-time"
          },
          "watch_minutes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Total minutes watched, all viewers together."
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where `peak` and `watch_minutes` come from: `cdn` (every player, exact) or `player` (hosted player estimate, until the CDN count arrives).",
            "enum": [
              "cdn",
              "player",
              null
            ]
          },
          "cdn_counted_until": {
            "type": [
              "string",
              "null"
            ],
            "description": "CDN logs are read up to this time.",
            "format": "date-time"
          }
        }
      },
      "ViewerSeries": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `viewer_series`.",
            "const": "viewer_series"
          },
          "stream_id": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "player": {
            "type": "array",
            "description": "Per minute, from the hosted player (live estimate).",
            "items": {
              "type": "object",
              "properties": {
                "minute": {
                  "type": "string",
                  "description": "",
                  "format": "date-time"
                },
                "watching": {
                  "type": "integer",
                  "description": ""
                },
                "waiting": {
                  "type": "integer",
                  "description": ""
                },
                "buffering": {
                  "type": "integer",
                  "description": ""
                }
              }
            }
          },
          "cdn": {
            "type": "array",
            "description": "Per minute, from the CDN logs: average viewers watching at once, every player included.",
            "items": {
              "type": "object",
              "properties": {
                "minute": {
                  "type": "string",
                  "description": "",
                  "format": "date-time"
                },
                "viewers": {
                  "type": "number"
                }
              }
            }
          },
          "countries": {
            "type": "array",
            "description": "Where the watch time came from, from the CDN logs (every player), largest first, up to 30. Grows hour by hour while the stream runs.",
            "items": {
              "type": "object",
              "properties": {
                "country": {
                  "type": "string",
                  "description": "ISO 3166-1 alpha-2 code, or `??` when the CDN could not tell."
                },
                "watch_minutes": {
                  "type": "integer",
                  "description": "Minutes watched from that country, all viewers together."
                },
                "share_pct": {
                  "type": "number",
                  "description": "Share of all watch time, in percent."
                }
              }
            }
          }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `webhook_endpoint`.",
            "const": "webhook_endpoint"
          },
          "id": {
            "type": "string",
            "description": "`whk_…`"
          },
          "url": {
            "type": "string",
            "description": "Where events are POSTed. https only."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own note."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "stream.live",
                "stream.ended",
                "stream.expired",
                "stream.cancelled",
                "recording.ready",
                "recording.failed",
                "recording.deleted"
              ]
            },
            "description": "Event types sent to this endpoint."
          },
          "all_events": {
            "type": "boolean",
            "description": "True when subscribed to everything, including event types added later."
          },
          "enabled": {
            "type": "boolean",
            "description": "False: nothing is sent."
          },
          "created_at": {
            "type": "string",
            "description": "",
            "format": "date-time"
          },
          "secret": {
            "type": "string",
            "description": "`whsec_…`, for verifying signatures. Only on create, `GET /v1/webhooks/{id}` and rotation."
          }
        }
      },
      "WebhookEndpointList": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEndpoint"
            }
          },
          "event_types": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every type an endpoint can subscribe to."
          }
        }
      },
      "WebhookDeliveryList": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookDelivery"
            }
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `webhook_delivery`.",
            "const": "webhook_delivery"
          },
          "id": {
            "type": "string",
            "description": "`dlv_…`"
          },
          "event_id": {
            "type": "string",
            "description": "`evt_…`, the `webhook-id` header."
          },
          "event_type": {
            "type": "string",
            "description": ""
          },
          "stream_id": {
            "type": [
              "string",
              "null"
            ],
            "description": ""
          },
          "status": {
            "type": "string",
            "description": "",
            "enum": [
              "pending",
              "succeeded",
              "failed"
            ]
          },
          "attempts": {
            "type": "integer",
            "description": ""
          },
          "max_attempts": {
            "type": "integer",
            "description": ""
          },
          "next_attempt_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "",
            "format": "date-time"
          },
          "last_attempt_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "",
            "format": "date-time"
          },
          "last_status_code": {
            "type": [
              "integer",
              "null"
            ],
            "description": "What your server answered."
          },
          "last_error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the last attempt failed (`answered 500`, a timeout, a DNS error, …)."
          },
          "last_duration_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": ""
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "description": "",
            "format": "date-time"
          },
          "payload": {
            "$ref": "#/components/schemas/WebhookEvent"
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "description": "The body of every webhook. Headers: `webhook-id`, `webhook-timestamp`, `webhook-signature` (Standard Webhooks).",
        "properties": {
          "type": {
            "type": "string",
            "description": "",
            "enum": [
              "stream.live",
              "stream.ended",
              "stream.expired",
              "stream.cancelled",
              "recording.ready",
              "recording.failed",
              "recording.deleted",
              "webhook.test"
            ]
          },
          "timestamp": {
            "type": "string",
            "description": "When it happened.",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "description": "For stream and recording events, the stream at that moment. For `webhook.test`, the endpoint.",
            "properties": {
              "object": {
                "type": "string",
                "description": "",
                "enum": [
                  "stream",
                  "webhook_endpoint"
                ]
              },
              "id": {
                "type": "string",
                "description": ""
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": ""
              },
              "status": {
                "type": "string",
                "description": ""
              },
              "metadata": {
                "type": [
                  "object",
                  "null"
                ]
              },
              "created_at": {
                "type": "string",
                "description": "",
                "format": "date-time"
              },
              "went_live_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "",
                "format": "date-time"
              },
              "ended_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "",
                "format": "date-time"
              },
              "end_reason": {
                "type": [
                  "string",
                  "null"
                ],
                "description": ""
              },
              "recording": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "description": ""
                  },
                  "bytes": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": ""
                  },
                  "duration_seconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "ready_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "",
                    "format": "date-time"
                  },
                  "deleted_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "",
                    "format": "date-time"
                  }
                }
              },
              "message": {
                "type": "string",
                "description": "Only on `webhook.test`."
              }
            }
          }
        }
      },
      "Stream": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "object": {
            "type": "string",
            "description": "Always `stream`.",
            "const": "stream"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your label, up to 100 characters. Shown on the hosted player unless `player.show_name` is false."
          },
          "status": {
            "type": "string",
            "description": "`pending` (waiting for the publisher, up to 30 min), `live`, `ended`, `expired` (nothing connected in time) or `cancelled` (deleted before going live).",
            "enum": [
              "pending",
              "live",
              "ended",
              "expired",
              "cancelled"
            ]
          },
          "profile": {
            "type": "string",
            "description": "`abr_basic`: several qualities for viewers. `passthrough`: exactly what you send.",
            "enum": [
              "abr_basic",
              "passthrough"
            ]
          },
          "record": {
            "type": "boolean",
            "description": "Whether an MP4 is made after the stream ends."
          },
          "recording": {
            "$ref": "#/components/schemas/Recording"
          },
          "node_ready": {
            "type": "boolean",
            "description": "The ingest server is up. False for about a minute when a new server has to start."
          },
          "rtmp_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "RTMP server URL for OBS or ffmpeg. Use with `stream_key`."
          },
          "playback_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Signed HLS URL for your own player. Null once the stream is finished (there is no replay). Re-signed on every read."
          },
          "playback_url_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When `playback_url` stops working.",
            "format": "date-time"
          },
          "embed_url": {
            "type": "string",
            "description": "The hosted player page. Put it in an iframe or share it as a link.",
            "examples": [
              "https://play.joacdn.com/str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "renditions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Qualities viewers will get, e.g. `[\"source\", \"480p\"]`."
          },
          "max_ingest_resolution": {
            "type": "integer",
            "description": "The tallest video you declared you will send: 480, 720 or 1080."
          },
          "max_duration_seconds": {
            "type": "integer",
            "description": "The stream ends after this long live. 4 h for new accounts, 24 h for trusted ones."
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "description": "Your own JSON, up to 2 KB. Never shown to viewers."
          },
          "player": {
            "$ref": "#/components/schemas/PlayerSettings"
          },
          "reservation_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "While pending: when the reserved slot is released and the stream expires.",
            "format": "date-time"
          },
          "reservation_expires_in_seconds": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The same as a countdown, computed by the server."
          },
          "created_at": {
            "type": "string",
            "description": "",
            "format": "date-time"
          },
          "went_live_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "",
            "format": "date-time"
          },
          "ended_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "",
            "format": "date-time"
          },
          "end_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why it ended: `deleted`, `disconnected`, `duration_limit`, `reservation_ttl`, `limit_bitrate`, `limit_resolution`, `insufficient_credit`, `simulcast_not_supported`, `provision_failed`."
          },
          "ingest": {
            "$ref": "#/components/schemas/Ingest"
          },
          "cost": {
            "$ref": "#/components/schemas/Cost"
          },
          "viewers": {
            "$ref": "#/components/schemas/Viewers"
          },
          "viewers_now": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Watching on the hosted player right now (the same estimate as `viewers.now.watching`). Null unless the stream is pending or live and someone is on the player. Also in the list, so one call shows every live audience."
          }
        }
      },
      "CreatedStream": {
        "description": "A stream plus its secrets. Returned once, by the create call (and by replacing the key). Store them; they are never shown again.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Stream"
          },
          {
            "type": "object",
            "properties": {
              "stream_key": {
                "type": "string",
                "description": "RTMP stream key, `{id}?key={secret}`. OBS: paste as Stream Key with `rtmp_url` as Server. One-shot: works for this stream only."
              },
              "whip_url": {
                "type": "string",
                "description": "WHIP endpoint for browsers and WebRTC encoders. Contains the secret."
              }
            }
          }
        ]
      },
      "StreamList": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Stream"
            }
          },
          "has_more": {
            "type": "boolean",
            "description": "More streams after the last one. Pass its id as `starting_after`."
          }
        }
      },
      "CreateStreamRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Up to 100 characters. Blank means no name."
          },
          "profile": {
            "type": "string",
            "description": "Default `abr_basic`.",
            "enum": [
              "abr_basic",
              "passthrough"
            ]
          },
          "max_resolution": {
            "type": "integer",
            "description": "The tallest video you will send. Default 720. 1080 needs a trusted account. Sending more than you declare ends the stream after a 20 s warning.",
            "enum": [
              480,
              720,
              1080
            ]
          },
          "record": {
            "type": "boolean",
            "description": "Make an MP4 after the stream ends. Default true."
          },
          "recording_retention_days": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Days to keep the MP4, 1–3650. Omit for the account default (30). Null: keep until deleted.",
            "minimum": 1,
            "maximum": 3650
          },
          "metadata": {
            "type": "object",
            "description": "Your own JSON, up to 2 KB."
          },
          "player": {
            "$ref": "#/components/schemas/PlayerSettings"
          }
        }
      },
      "UpdateStreamRequest": {
        "type": "object",
        "additionalProperties": false,
        "minProperties": 1,
        "description": "Only these fields can change, in any status. Null clears a field; `metadata` is replaced whole.",
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Up to 100 characters."
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ]
          },
          "player": {
            "$ref": "#/components/schemas/PlayerSettings"
          }
        }
      },
      "PlaybackToken": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `playback_token`.",
            "const": "playback_token"
          },
          "stream_id": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "playback_url": {
            "type": "string",
            "description": "Signed HLS URL."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "",
            "format": "date-time"
          }
        }
      },
      "RecordingDownload": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `recording_download`.",
            "const": "recording_download"
          },
          "stream_id": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "url": {
            "type": "string",
            "description": "Signed link to the MP4 on the CDN. Downloading it counts as delivery."
          },
          "bytes": {
            "type": "integer",
            "description": "MP4 size."
          },
          "expires_at": {
            "type": "string",
            "description": "",
            "format": "date-time"
          }
        }
      },
      "Embed": {
        "type": "object",
        "description": "What the hosted player needs. Public: no key, any origin may read it.",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `embed`.",
            "const": "embed"
          },
          "id": {
            "type": "string",
            "description": "Stream id. Also the folder of its video on the CDN.",
            "pattern": "^str_[a-z0-9]{20}$",
            "examples": [
              "str_e1bzk3dxw9z9allei6n7"
            ]
          },
          "status": {
            "type": "string",
            "description": "`pending`, `live`, `ended`, or `unavailable` (expired or cancelled).",
            "enum": [
              "pending",
              "live",
              "ended",
              "unavailable"
            ]
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "The stream name when it is to be shown, otherwise null."
          },
          "playback_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "While live: a signed HLS URL, the same for every viewer."
          },
          "playback_url_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "",
            "format": "date-time"
          },
          "went_live_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "",
            "format": "date-time"
          },
          "ended_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "",
            "format": "date-time"
          },
          "poll_after_seconds": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Ask again after this many seconds (5 pending, 60 live). Null: stop asking."
          },
          "beacon": {
            "type": [
              "object",
              "null"
            ],
            "description": "Viewer counting: a player whose random number is below `rate` reports every `interval_seconds` (see Count viewers from your own player). Null once the stream is over.",
            "properties": {
              "rate": {
                "type": "number"
              },
              "interval_seconds": {
                "type": "integer",
                "description": ""
              }
            }
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `usage`.",
            "const": "usage"
          },
          "balance_usd": {
            "type": "string",
            "description": "Credit minus everything charged so far.",
            "pattern": "^-?\\d+\\.\\d+$",
            "examples": [
              "0.016790"
            ]
          },
          "available_usd": {
            "type": "string",
            "description": "Balance minus used-but-not-yet-charged. Live streams end when this reaches zero.",
            "pattern": "^-?\\d+\\.\\d+$",
            "examples": [
              "0.016790"
            ]
          },
          "balance_depleted_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set while the balance is at or below zero.",
            "format": "date-time"
          },
          "recordings_delete_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "7 days after the balance reached zero: when recordings will be deleted unless you top up.",
            "format": "date-time"
          },
          "rates": {
            "type": "object",
            "properties": {
              "delivery_usd_per_gb": {
                "type": "string",
                "description": "Per GB delivered to viewers.",
                "pattern": "^-?\\d+\\.\\d+$",
                "examples": [
                  "0.016790"
                ]
              },
              "transcode_usd_per_minute": {
                "type": "string",
                "description": "Per minute of an abr_basic stream.",
                "pattern": "^-?\\d+\\.\\d+$",
                "examples": [
                  "0.016790"
                ]
              },
              "passthrough_usd_per_minute": {
                "type": "string",
                "description": "Per minute of a passthrough stream.",
                "pattern": "^-?\\d+\\.\\d+$",
                "examples": [
                  "0.016790"
                ]
              },
              "storage_usd_per_gb_month": {
                "type": "string",
                "description": "Per GB-month of recordings.",
                "pattern": "^-?\\d+\\.\\d+$",
                "examples": [
                  "0.016790"
                ]
              }
            }
          },
          "days": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "description": "UTC day.",
                  "format": "date"
                },
                "delivery_bytes": {
                  "type": "integer",
                  "description": ""
                },
                "transcode_seconds": {
                  "type": "integer",
                  "description": ""
                },
                "passthrough_seconds": {
                  "type": "integer",
                  "description": ""
                },
                "storage_gb_hours": {
                  "type": "number"
                },
                "watch_minutes": {
                  "type": "integer",
                  "description": "Minutes watched that day by all viewers together, every player included, from the CDN logs."
                },
                "cost_usd": {
                  "type": "string",
                  "description": "",
                  "pattern": "^-?\\d+\\.\\d+$",
                  "examples": [
                    "0.016790"
                  ]
                },
                "charged": {
                  "type": "boolean",
                  "description": "False until the day is closed and debited, about 2 h after it ends."
                }
              }
            }
          }
        }
      }
    }
  }
}
