{
  "openapi": "3.0.3",
  "info": {
    "title": "Eventdoor API",
    "description": "Automate your platform with a scoped API credential. This reference lists every operation that accepts one; no other operation does.\n\n## Issue a credential\n\nIn the admin, open **Settings > Integrations**. An organization's owners and admins issue credentials for that organization; platform admins issue platform-wide ones from the platform's own Settings > Integrations. Choose a name, a level (`read` or `write`) and how long it works: 30, 90 or 365 days (365 unless you choose otherwise). There is no credential that never expires.\n\nThe token (`edk_...`) is shown once, when it is issued. Only a hash of it is stored, so it cannot be shown again: keep it in a secret store. Revoking a credential in the same place stops it on its next request.\n\n## Authenticate\n\nSend the token as a bearer token on every request:\n\n```\nAuthorization: Bearer edk_...\n```\n\nA credential is not a user: it has no profile, is not a member, and keeps working when the person who issued it leaves.\n\n## What a credential may do\n\n- **`read`** makes GET requests only, acting as a member of the organization.\n- **`write`** acts as an admin of the organization.\n- An organization's credential reaches that organization only. A platform-wide credential reaches every organization on the platform except internal ones; a `write` one acts as an admin in each.\n- No credential ever acts as an owner or a system admin, and none unlocks paid content.\n\n## Refusals\n\n- **401** `Invalid API credential`: the token is malformed or unknown, its secret is wrong, or its organization has been deleted. `This API credential has been revoked` and `This API credential has expired` say which. A token in the `edk_...` form that does not authenticate is refused on every operation, including ones open to anonymous callers. A bearer token that does not start with `edk_` (lowercase) is not treated as a credential: on an operation open to anonymous callers it is served anonymously, with no error. The response carries `WWW-Authenticate: Bearer error=\"invalid_token\"`.\n- **403** `This operation does not accept API credentials`: the operation is not in this reference.\n- **403** `This API credential is read-only: it may make GET requests only`.\n- **403** `This API credential is scoped to the organization <slug>`: the path names another organization.\n- **403** `This API credential is scoped to the organization <slug>; this operation is not`: the operation names no organization.\n- **403** `A read-only API credential cannot read change history`: a broadcast's change history needs a `write` credential.\n- **400** `A bulk call may carry at most 50 operations`: a credential's bulk update or bulk upsert carries at most 50 items in `operations`. Split a larger batch into several calls.\n\nEvery error body is JSON with the reason in `message`.\n\n## Rate limit\n\nEach credential has its own limit: a burst of requests at once (60 by default), refilled at a steady rate per minute (120 by default). The platform's operator may set other values, and a credential may occasionally be limited a little sooner or later than that, so always handle **429** rather than counting on an exact number. A bulk call counts one request per item in `operations`.\n\nOver the limit, a request gets **429** with a `Retry-After` header (seconds) and this body:\n\n```json\n{\n  \"message\": \"Too many requests from this API credential. Retry after 3 seconds.\",\n  \"errorCode\": \"rate_limited\",\n  \"retryAfterSeconds\": 3\n}\n```\n\nWait `Retry-After` seconds before retrying.",
    "version": "1.0.0-next.83"
  },
  "servers": [
    {
      "url": "https://{apiHost}/eventdoor/api/v1",
      "variables": {
        "apiHost": {
          "default": "api.example.com",
          "description": "Your platform's API host. Copy the API base URL from Settings > Integrations in the admin."
        }
      }
    }
  ],
  "tags": [
    {
      "name": "Broadcasts",
      "description": "Hub Broadcasts"
    },
    {
      "name": "Hubs",
      "description": "Organization Hubs"
    }
  ],
  "paths": {
    "/organizations/{orgSlug}/hubs": {
      "parameters": [
        {
          "$ref": "#/components/parameters/orgSlugParam"
        }
      ],
      "get": {
        "parameters": [
          {
            "$ref": "#/components/parameters/limitQueryAll"
          },
          {
            "$ref": "#/components/parameters/cursorQuery"
          },
          {
            "$ref": "#/components/parameters/hubQueryQuery"
          },
          {
            "$ref": "#/components/parameters/parentIdQuery"
          },
          {
            "$ref": "#/components/parameters/facetsQuery"
          },
          {
            "$ref": "#/components/parameters/hubStateQuery"
          },
          {
            "$ref": "#/components/parameters/startsAfterQuery"
          },
          {
            "$ref": "#/components/parameters/startsBeforeQuery"
          },
          {
            "$ref": "#/components/parameters/endsAfterQuery"
          },
          {
            "$ref": "#/components/parameters/endsBeforeQuery"
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Filter by tag slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tagCategory",
            "in": "query",
            "required": false,
            "description": "Filter by tag-category slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collection",
            "in": "query",
            "required": false,
            "description": "Filter by collection slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionCategory",
            "in": "query",
            "required": false,
            "description": "Filter by collection-category slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionItem",
            "in": "query",
            "required": false,
            "description": "Filter by collection-item id (UUID).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "tags": [
          "Hubs"
        ],
        "description": "Filter by tag categories using query params named after category slugs (e.g. ?age_group=5-8,9-12&sport=swimming). Within a category, values are OR'd; across categories, they are AND'd. Also supports single-value tag / collection / category filters. Use facets=true to include facet counts in the response.\n",
        "summary": "List organization hubs",
        "operationId": "listOrgHubs",
        "security": [
          {},
          {
            "ApiCredential": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HubList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/organizations/{orgSlug}/hubs/{hubSlug}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/orgSlugParam"
        },
        {
          "$ref": "#/components/parameters/hubSlugParam"
        }
      ],
      "get": {
        "parameters": [
          {
            "$ref": "#/components/parameters/expandQuery"
          },
          {
            "$ref": "#/components/parameters/depthQuery"
          }
        ],
        "tags": [
          "Hubs"
        ],
        "summary": "Get hub",
        "operationId": "getHub",
        "security": [
          {},
          {
            "ApiCredential": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HubResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/organizations/{orgSlug}/broadcasts": {
      "parameters": [
        {
          "$ref": "#/components/parameters/orgSlugParam"
        }
      ],
      "get": {
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of items to retrieve per page. Use \"all\" to retrieve all items without pagination.\n",
            "schema": {
              "oneOf": [
                {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 100
                },
                {
                  "type": "string",
                  "enum": [
                    "all"
                  ]
                }
              ]
            }
          },
          {
            "$ref": "#/components/parameters/cursorQuery"
          },
          {
            "$ref": "#/components/parameters/facetsQuery"
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Filter broadcasts by temporal state. \"upcoming\" returns broadcasts whose starts_at is in the future. \"active\" returns broadcasts in their scheduled window (starts_at <= now, ends_at null or in the future). \"live\" returns only broadcasts with an active stream. \"past\" returns broadcasts whose ends_at is in the past.\n",
            "schema": {
              "type": "string",
              "enum": [
                "upcoming",
                "active",
                "live",
                "past"
              ]
            }
          },
          {
            "name": "startsAfter",
            "in": "query",
            "required": false,
            "description": "Filter to broadcasts starting at or after this date-time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "startsBefore",
            "in": "query",
            "required": false,
            "description": "Filter to broadcasts starting at or before this date-time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "endsAfter",
            "in": "query",
            "required": false,
            "description": "Filter to broadcasts ending at or after this date-time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "endsBefore",
            "in": "query",
            "required": false,
            "description": "Filter to broadcasts ending at or before this date-time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Filter by tag slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tagCategory",
            "in": "query",
            "required": false,
            "description": "Filter by tag-category slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collection",
            "in": "query",
            "required": false,
            "description": "Filter by collection slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionCategory",
            "in": "query",
            "required": false,
            "description": "Filter by collection-category slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionItem",
            "in": "query",
            "required": false,
            "description": "Filter by collection-item id (UUID).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "description": "Filter by broadcast mode.",
            "schema": {
              "type": "string",
              "enum": [
                "live",
                "vod"
              ]
            }
          }
        ],
        "tags": [
          "Broadcasts"
        ],
        "description": "List broadcasts across all hubs belonging to the organization. Supports cross-hub discovery via tag / collection / category filters. Each row's `attachedVod.videoUrl` is null on a paid broadcast unless the caller has bought access to it or is signed in as a member of its organization. An API credential never unlocks paid content, so with one it is null on every paid broadcast.\n",
        "summary": "List broadcasts (org-wide)",
        "operationId": "listOrgBroadcasts",
        "security": [
          {},
          {
            "ApiCredential": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/organizations/{orgSlug}/broadcasts/{broadcastSlug}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/orgSlugParam"
        },
        {
          "$ref": "#/components/parameters/broadcastSlugParam"
        }
      ],
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Get broadcast (org-scoped)",
        "description": "Retrieve a broadcast by its slug within the organization scope. Broadcast slugs are unique per organization, so the hub slug is not required to resolve the broadcast.\n",
        "operationId": "getOrgBroadcast",
        "security": [
          {},
          {
            "ApiCredential": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastDetailResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Update broadcast (org-scoped)",
        "description": "Update a broadcast by its slug within the organization scope. On a VOD-mode broadcast with an attached video, a new title, description or posterUrl also updates that video.\n",
        "operationId": "updateOrgBroadcast",
        "security": [
          {
            "ApiCredential": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BroadcastUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Broadcast updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequestError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "409": {
            "$ref": "#/components/responses/ConflictError"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/organizations/{orgSlug}/broadcasts/{broadcastSlug}/changes": {
      "parameters": [
        {
          "$ref": "#/components/parameters/orgSlugParam"
        },
        {
          "$ref": "#/components/parameters/broadcastSlugParam"
        }
      ],
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "List who changed a broadcast, newest first",
        "description": "Each create, edit, end, reopen, stream key rotation and delete of the broadcast, with who made it and whether it came through the AI assistant or a scoped API credential. An edit lists the fields whose stored value changed; a save that changed nothing is not listed. Organization admins only. Cursor-paginated: pass `paging.next` back as `cursor`.\n",
        "operationId": "listBroadcastChanges",
        "security": [
          {
            "ApiCredential": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Changes per page (default 20).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "$ref": "#/components/parameters/cursorQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of changes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastChangeList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequestError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts": {
      "parameters": [
        {
          "$ref": "#/components/parameters/orgSlugParam"
        },
        {
          "$ref": "#/components/parameters/hubSlugParam"
        }
      ],
      "get": {
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of items to retrieve per page. Use \"all\" to retrieve all items without pagination.\n",
            "schema": {
              "oneOf": [
                {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 100
                },
                {
                  "type": "string",
                  "enum": [
                    "all"
                  ]
                }
              ]
            }
          },
          {
            "$ref": "#/components/parameters/cursorQuery"
          },
          {
            "$ref": "#/components/parameters/facetsQuery"
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Filter broadcasts by temporal state. \"upcoming\" returns broadcasts whose starts_at is in the future. \"active\" returns broadcasts in their scheduled window (starts_at <= now, ends_at null or in the future). \"live\" returns only broadcasts with an active stream (stream details are not fetched when omitted). \"past\" returns broadcasts whose ends_at is in the past.\n",
            "schema": {
              "type": "string",
              "enum": [
                "upcoming",
                "active",
                "live",
                "past"
              ]
            }
          },
          {
            "name": "startsAfter",
            "in": "query",
            "required": false,
            "description": "Filter to broadcasts starting at or after this date-time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "startsBefore",
            "in": "query",
            "required": false,
            "description": "Filter to broadcasts starting at or before this date-time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "endsAfter",
            "in": "query",
            "required": false,
            "description": "Filter to broadcasts ending at or after this date-time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "endsBefore",
            "in": "query",
            "required": false,
            "description": "Filter to broadcasts ending at or before this date-time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Filter by tag slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tagCategory",
            "in": "query",
            "required": false,
            "description": "Filter by tag-category slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collection",
            "in": "query",
            "required": false,
            "description": "Filter by collection slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionCategory",
            "in": "query",
            "required": false,
            "description": "Filter by collection-category slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionItem",
            "in": "query",
            "required": false,
            "description": "Filter by collection-item id (UUID).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "description": "Filter by broadcast mode.",
            "schema": {
              "type": "string",
              "enum": [
                "live",
                "vod"
              ]
            }
          }
        ],
        "tags": [
          "Broadcasts"
        ],
        "description": "Filter by tag categories using query params named after category slugs. Within a category, values are OR'd; across categories, they are AND'd. Use facets=true to include facet counts in the response. Each row's `attachedVod.videoUrl` is null on a paid broadcast unless the caller has bought access to it or is signed in as a member of its organization. An API credential never unlocks paid content, so with one it is null on every paid broadcast.\n",
        "summary": "List broadcasts in hub",
        "operationId": "listBroadcasts",
        "security": [
          {},
          {
            "ApiCredential": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Create broadcast",
        "operationId": "createBroadcast",
        "security": [
          {
            "ApiCredential": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Broadcast"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Broadcast created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequestError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "409": {
            "$ref": "#/components/responses/ConflictError"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/bulk": {
      "parameters": [
        {
          "$ref": "#/components/parameters/orgSlugParam"
        },
        {
          "$ref": "#/components/parameters/hubSlugParam"
        }
      ],
      "patch": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Bulk update broadcasts",
        "description": "Update multiple broadcasts in a single request. Each operation is processed independently (best-effort). Returns 207 with per-item results.\n",
        "operationId": "bulkUpdateBroadcasts",
        "security": [
          {
            "ApiCredential": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkUpdateBroadcastsRequest"
              }
            }
          }
        },
        "responses": {
          "207": {
            "description": "Multi-status response with per-item results",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkUpdateBroadcastsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequestError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/bulk-upsert": {
      "parameters": [
        {
          "$ref": "#/components/parameters/orgSlugParam"
        },
        {
          "$ref": "#/components/parameters/hubSlugParam"
        }
      ],
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Bulk upsert broadcasts",
        "description": "Create or update multiple broadcasts in a single request (e.g. a CSV import/round-trip). An operation with an `id` updates that broadcast in place; without one it creates a new broadcast. Each operation is processed independently (best-effort). Returns 207 with per-item results keyed by an optional client-supplied ref.\n",
        "operationId": "bulkUpsertBroadcasts",
        "security": [
          {
            "ApiCredential": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkUpsertBroadcastsRequest"
              }
            }
          }
        },
        "responses": {
          "207": {
            "description": "Multi-status response with per-item results",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkUpsertBroadcastsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequestError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/{broadcastSlug}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/orgSlugParam"
        },
        {
          "$ref": "#/components/parameters/hubSlugParam"
        },
        {
          "$ref": "#/components/parameters/broadcastSlugParam"
        }
      ],
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Get broadcast",
        "operationId": "getBroadcast",
        "security": [
          {},
          {
            "ApiCredential": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastDetailResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Update broadcast",
        "operationId": "updateBroadcast",
        "security": [
          {
            "ApiCredential": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BroadcastUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Broadcast updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequestError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "409": {
            "$ref": "#/components/responses/ConflictError"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "InternalServerError": {
        "description": "Internal Server Error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequestsError": {
        "description": "Too many requests - retry later",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Validation Error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ConflictError": {
        "description": "Conflict",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFoundError": {
        "description": "Not Found Error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ForbiddenError": {
        "description": "Forbidden",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UnauthorizedError": {
        "description": "Unauthorized",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequestError": {
        "description": "Bad Request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string",
            "description": "Error message for the end user"
          },
          "errorCode": {
            "type": "string",
            "description": "Code that can be set that supplies a specific reason for why a request failed"
          },
          "invalidFields": {
            "type": "array",
            "description": "Specific error scoped to a field",
            "items": {
              "$ref": "#/components/schemas/InvalidField"
            }
          },
          "internal": {
            "type": "string",
            "description": "Internal user only"
          },
          "reference": {
            "type": "string",
            "description": "Short identifier for an unexpected failure, matching the API log entry for it. Safe to show the user so they can quote it to support.\n"
          }
        }
      },
      "InvalidField": {
        "type": "object",
        "required": [
          "name",
          "reason"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          },
          "errorCode": {
            "type": "string"
          }
        }
      },
      "BroadcastResponse": {
        "type": "object",
        "required": [
          "id",
          "mode",
          "kind",
          "organizationId",
          "title",
          "slug"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "mode": {
            "type": "string",
            "enum": [
              "live",
              "vod"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "live",
              "vod",
              "archive"
            ],
            "description": "UI discriminator: 'live'/'vod' track mode, plus 'archive' when the attached VOD was captured from a live stream (source='live-archive'). Archiving attaches the VOD to the same broadcast and flips it to mode='vod' — the slug, URL, and schedule are unchanged.\n"
          },
          "hubId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "organizationId": {
            "type": "string",
            "format": "uuid"
          },
          "vodId": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "ID of the attached VOD, or null when the broadcast is live."
          },
          "attachedVod": {
            "$ref": "#/components/schemas/AttachedVodSummary"
          },
          "clipStartsAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Archive-only. The in-point of the WHOLE-RECORDING archive window - the admin shows this as the \"Archive trim\".\n\nNOT a clip. A clip is a separate resource with its own `startTime`/`endTime`; this pair bounds the entire recording, of which there is exactly one per broadcast. Set at archive time: the scheduled start minus the pre-roll padding on a dedicated stream key, the exact scheduled start on a shared key or a forced archive.\n"
          },
          "clipEndsAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Archive-only. The out-point of the WHOLE-RECORDING archive window - the admin shows this as the \"Archive trim\".\n\nNOT a clip; see `clipStartsAt`. Set at archive time: on a dedicated stream key, whichever comes first of the stream stopping and the scheduled end plus the post-roll padding; the exact scheduled end on a shared key; the operator's cut time on a forced archive.\n"
          },
          "archiveStartedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The recorded footage's REAL first-frame time — the VOD playback timeline's 0:00. A stream rarely goes live exactly at its scheduled `startsAt`, so this is later by the \"late-start delta\". Display and recording-bounds only. It is not a clip anchor: clip positions come from `offsetIntoReplaySec`. Null for a broadcast that is not a completed live archive, or one archived before this field existed.\n"
          },
          "endedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Operator-declared end of the live window, or null. When set the broadcast is NOT live regardless of whether its stream key is still publishing, and this instant is the end bound of its archive merge. Set by `endBroadcastLive`, cleared by `reopenBroadcastLive`.\n"
          },
          "endedBy": {
            "type": "string",
            "nullable": true,
            "description": "The user who force-ended this broadcast, or null."
          },
          "endReason": {
            "type": "string",
            "nullable": true,
            "description": "Operator-supplied note explaining the force-end, or null."
          },
          "archiveAttemptedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When an archive run last finished without retrying. A forced run sets it on every outcome; a scheduled run sets it when the merge comes back empty or fails its last retry, and it is cleared when the schedule is saved again (a fresh attempt). This is how a caller tells \"ended, archive still running\" (null) from \"ended, archive ran and produced no replay\" (set, with `mode` still `live`). When `mode` is `vod` the run produced the attached replay.\n"
          },
          "programId": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The auto-created default program for this broadcast. Null for VOD-mode broadcasts."
          },
          "activeProgramId": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The resolved active program — linked program from a program collection if one exists, otherwise falls back to programId. Null for VOD-mode broadcasts.\n"
          },
          "hub": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              }
            }
          },
          "organization": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              },
              "avatarUrl": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "posterUrl": {
            "type": "string",
            "nullable": true,
            "description": "The EFFECTIVE poster: a manual poster wins over a template-generated one; null falls through to the surface placeholder."
          },
          "generatedPosterUrl": {
            "type": "string",
            "nullable": true,
            "description": "The raw template-generated poster, if one is baked. Exposed alongside posterUrl so a client can tell which poster is in effect."
          },
          "posterBakeFailed": {
            "type": "boolean",
            "description": "Transient, mutation responses only: this save's poster re-bake hit an operational failure (storage/composition), so generatedPosterUrl may be STALE — the previous bake's URL, not this save's. Without this flag a failed RE-bake is indistinguishable from success (the old non-null URL rides along). Absent means the bake succeeded, was skipped, or wasn't attempted."
          },
          "graphicsTemplateId": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The graphics template applied to this broadcast, or null."
          },
          "useGeneratedPoster": {
            "type": "boolean",
            "description": "Per-broadcast poster-generation toggle."
          },
          "streamId": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Null for VOD-mode broadcasts."
          },
          "startsAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "endsAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "nullable": true
          },
          "archiveEnabled": {
            "type": "boolean",
            "description": "Live-mode only. Whether this broadcast will be archived to a replay VOD after it ends. Always true for archives created before this toggle existed.\n"
          },
          "visibility": {
            "$ref": "#/components/schemas/BroadcastVisibility"
          },
          "archivePreRollMinutes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 240,
            "nullable": true,
            "description": "Live mode, dedicated stream key only. Minutes before `startsAt` that the live-archive merge window covers. Null (the default) uses the platform default. A broadcast whose stream key is shared with other broadcasts ignores padding entirely, because padding would pull in the neighbouring broadcast's footage. Applies only to an archive that has not run yet; changing it later does not re-create an existing recording. Set through the API only; the admin has no field for it.\n"
          },
          "archivePostRollMinutes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 720,
            "nullable": true,
            "description": "Live mode, dedicated stream key only. Minutes after `endsAt` that the live-archive merge window may cover. The merge ends at whichever comes first, the stream actually stopping or this padded end, so raising it does not extend a replay past the footage. Null (the default) uses the platform default. Raise it for an event that legitimately runs long rather than raising the platform default for everyone. Applies only to an archive that has not run yet; changing it later does not re-create an existing recording.\n"
          },
          "live": {
            "type": "boolean"
          },
          "manifestUrl": {
            "type": "string",
            "nullable": true
          },
          "defaultStreamPublicKey": {
            "type": "string",
            "nullable": true
          },
          "streams": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProgramStreamProducerResponse"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GroupedTags"
            }
          },
          "collectionItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BroadcastCollectionItemResponse"
            }
          },
          "madeFrom": {
            "type": "object",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ReplaySourceLink"
              }
            ],
            "description": "Single-broadcast reads (hub- and org-scoped), ORG ADMINS ONLY: the replay this broadcast was made from, or null when it was not made from one. Absent for everyone else — the source may be a draft or unlisted.\n"
          },
          "isMadeReplay": {
            "type": "boolean",
            "description": "List reads (`listBroadcasts`, `listOrgBroadcasts`), ORG ADMINS ONLY: true when this broadcast is a made replay (a window of another broadcast's recording), false otherwise. Absent for everyone else — viewers see a made replay as an ordinary replay. A flag rather than a `kind` value because `kind` is public and drives viewer watchability: a made replay's `kind` stays `archive`. Single reads omit it; `madeFrom != null` answers the same question there.\n"
          },
          "gate": {
            "type": "object",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ContentGateSummary"
              }
            ],
            "description": "PUBLIC monetization badge. Null when the content is free / ungated. Present on list responses; single-broadcast responses omit it. Carries no viewer entitlement data.\n"
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "updated": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ContentGateSummary": {
        "type": "object",
        "description": "PUBLIC monetization badge for a list row: whether the content is gated and, when paid, the display fields for the buy affordance. Contains NO viewer entitlement / \"owned\" data — list responses stay cacheable; \"owned\" is computed client-side. Free / ungated content carries no summary (the `gate` property is null). Assembled from the effective gate (hub→broadcast inheritance) the same way as the viewer content-access read.\n",
        "required": [
          "tier",
          "origin",
          "gatedContentType",
          "gatedContentId",
          "showPrice"
        ],
        "properties": {
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "paid"
            ],
            "description": "Access tier of the effective gate (a summary is only present for paid)."
          },
          "origin": {
            "type": "string",
            "enum": [
              "direct",
              "inherited",
              "none"
            ],
            "description": "Where the applied gate lives — the item's own gate, or one inherited from its hub or a collection."
          },
          "gatedContentType": {
            "type": "string",
            "enum": [
              "broadcast",
              "hub",
              "collection",
              "content_type"
            ],
            "description": "The content the applied gate actually lives on (itself, or the hub or collection it inherited from). A viewer's entitlement is keyed to this content."
          },
          "gatedContentId": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string",
            "nullable": true,
            "description": "Viewer-facing product label (falls back to the internal name)."
          },
          "buttonLabel": {
            "type": "string",
            "nullable": true,
            "description": "The gate's buy-button label; null falls back to the product label."
          },
          "showPrice": {
            "type": "boolean"
          },
          "unitAmount": {
            "type": "integer",
            "nullable": true,
            "description": "Price in minor units (e.g. cents)."
          },
          "currency": {
            "type": "string",
            "nullable": true,
            "description": "ISO currency code."
          },
          "recurringInterval": {
            "type": "string",
            "enum": [
              "day",
              "week",
              "month",
              "year"
            ],
            "description": "Cadence when the gate is a subscription plan; absent for one-time."
          }
        }
      },
      "ReplaySourceLink": {
        "type": "object",
        "description": "The replay a broadcast was made from. Returned to org admins only.",
        "required": [
          "sourceBroadcastId",
          "sourceDeleted"
        ],
        "properties": {
          "sourceBroadcastId": {
            "type": "string",
            "format": "uuid"
          },
          "sourceTitle": {
            "type": "string",
            "nullable": true,
            "description": "The source's title, kept after it is deleted. NULL only when its row is gone altogether."
          },
          "sourceDeleted": {
            "type": "boolean",
            "description": "Whether the source replay is deleted."
          },
          "sourceSlug": {
            "type": "string",
            "nullable": true,
            "description": "NULL once the source replay is deleted."
          },
          "sourceHubSlug": {
            "type": "string",
            "nullable": true,
            "description": "The source replay's own hub, which can differ from this broadcast's (either can be moved between hubs). NULL when the source is deleted, has no hub, or its hub is deleted."
          },
          "startTime": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "endTime": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "BroadcastCollectionItemResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CollectionItemResponse"
          },
          {
            "type": "object",
            "properties": {
              "qualifier": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "key": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  }
                }
              }
            }
          }
        ]
      },
      "CollectionItemResponse": {
        "type": "object",
        "required": [
          "id",
          "collectionId",
          "position"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "collectionId": {
            "type": "string",
            "format": "uuid"
          },
          "collection": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              },
              "category": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "type": {
            "type": "object",
            "nullable": true,
            "properties": {
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "system": {
                "type": "boolean"
              },
              "imageStyle": {
                "$ref": "#/components/schemas/ImageStyle"
              },
              "icon": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "entityId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "slug": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "avatarUrl": {
            "type": "string",
            "nullable": true
          },
          "bannerUrl": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "colors": {
            "type": "object",
            "nullable": true,
            "properties": {
              "primary": {
                "type": "string",
                "nullable": true
              },
              "secondary": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "fieldValues": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "position": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GroupedTags"
            }
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "streams": {
            "type": "array",
            "description": "Streams for program-type collection items. Empty array for non-program types.\n",
            "items": {
              "$ref": "#/components/schemas/ProgramStreamProducerResponse"
            }
          },
          "entity": {
            "type": "object",
            "nullable": true
          },
          "gate": {
            "type": "object",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ContentGateSummary"
              }
            ],
            "description": "PUBLIC monetization badge for the item's underlying content (broadcast or hub). Null when free / ungated or the item type carries no gate. Present on list responses. Carries no viewer entitlement data.\n"
          },
          "organizations": {
            "type": "array",
            "description": "Organizations linked to this item (empty when none). Present on single-item responses and enriched into list responses.\n",
            "items": {
              "$ref": "#/components/schemas/LinkedOrganization"
            }
          }
        }
      },
      "LinkedOrganization": {
        "type": "object",
        "required": [
          "slug"
        ],
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "slug": {
            "type": "string"
          },
          "avatarUrl": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ProgramStreamProducerResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/StreamDetails"
          },
          {
            "type": "object",
            "required": [
              "name",
              "publicKey"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "publicKey": {
                "type": "string"
              },
              "authKey": {
                "type": "string",
                "description": "Admin-only; omitted for non-admin viewers (can be used to publish)."
              },
              "streamKey": {
                "type": "string",
                "description": "Admin-only; omitted for non-admin viewers. `{publicKey}?authKey={authKey}&ns={projectId}`"
              },
              "projectId": {
                "type": "string",
                "description": "Admin-only. Foundation program service project identifier (same value embedded as `ns` in streamKey)."
              }
            }
          }
        ]
      },
      "StreamDetails": {
        "type": "object",
        "description": "Details from the foundation program service",
        "properties": {
          "live": {
            "type": "boolean"
          },
          "manifestUrl": {
            "type": "string",
            "nullable": true
          },
          "previewManifestUrl": {
            "type": "string",
            "nullable": true,
            "description": "Org admins only. The linked source's live manifest while the broadcast is outside its live window (`live` false, `manifestUrl` and `snapshotUrl` null), so an admin can check the camera before the window opens or after it closes. Absent inside the window and for a broadcast on its own stream key."
          },
          "snapshotUrl": {
            "type": "string",
            "nullable": true,
            "description": "Live frame snapshot (webp). Present only while live. Append ?w= & h= to size and a cache-busting param (e.g. &t=) to refresh."
          },
          "ingress": {
            "type": "object",
            "properties": {
              "rtmp": {
                "type": "string",
                "nullable": true
              },
              "webrtc": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "transcode": {
            "type": "boolean"
          },
          "authType": {
            "type": "string",
            "nullable": true
          },
          "appData": {
            "type": "string",
            "nullable": true
          },
          "rtmpPush": {
            "type": "string",
            "nullable": true,
            "description": "JSON-encoded RTMP push configuration"
          },
          "mode": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "GroupedTags": {
        "type": "object",
        "properties": {
          "category": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "values": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "slug": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ImageStyle": {
        "type": "string",
        "enum": [
          "avatar",
          "banner"
        ],
        "default": "avatar"
      },
      "BroadcastVisibility": {
        "type": "string",
        "enum": [
          "published",
          "unlisted",
          "draft"
        ],
        "default": "published",
        "description": "Who can find and open the broadcast. `published` is listed everywhere. `unlisted` is absent from every public list, search, schedule, collection, clip list and live-now feed, but its page, playback and purchases keep working for anyone with the link or a purchase. `draft` is unlisted AND its page returns 404 to anyone who is not an org member; a draft can still go live. Org admins see all three in lists."
      },
      "AttachedVodSummary": {
        "type": "object",
        "nullable": true,
        "required": [
          "id",
          "status",
          "originalArchiveAvailable"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "queued",
              "processing",
              "ready",
              "failed"
            ]
          },
          "videoUrl": {
            "type": "string",
            "nullable": true,
            "description": "Present when status is \"ready\""
          },
          "videoDuration": {
            "type": "integer",
            "nullable": true
          },
          "posterUrl": {
            "type": "string",
            "nullable": true,
            "description": "Thumbnail URL for the attached VOD. May be a user-set value or auto-derived from the transcoded video when transcoding completes.\n"
          },
          "source": {
            "type": "string",
            "enum": [
              "upload",
              "live-archive"
            ],
            "description": "How the VOD was created."
          },
          "fileName": {
            "type": "string",
            "nullable": true,
            "description": "Original filename of the source upload."
          },
          "xcodeUploadId": {
            "type": "integer",
            "nullable": true,
            "description": "Transcode service upload reference. Null when the file has not yet been submitted."
          },
          "originalArchiveAvailable": {
            "type": "boolean",
            "description": "True when this VOD was a live capture replaced by an upload and the original capture is still stashed — revertible via the VOD `restore-archive` endpoint.\n"
          },
          "archiveReplacedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the original live capture was replaced, or null when none is stashed."
          }
        }
      },
      "BroadcastUpdate": {
        "type": "object",
        "description": "Broadcast update payload. Slug uniqueness is scoped to the organization, not the hub.\n",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 72
          },
          "slug": {
            "type": "string",
            "maxLength": 72
          },
          "description": {
            "type": "string",
            "nullable": true,
            "maxLength": 240
          },
          "posterUrl": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "streamId": {
            "type": "string",
            "format": "uuid"
          },
          "startsAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "endsAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "maxLength": 64,
            "nullable": true
          },
          "graphicsTemplateId": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Apply a graphics template to this broadcast, or null to remove it. The generated poster is (re)composed from the broadcast's linked collection items; a manual posterUrl still wins."
          },
          "useGeneratedPoster": {
            "type": "boolean",
            "description": "Per-broadcast poster-generation toggle. Turning it off restores the manual poster."
          },
          "archiveEnabled": {
            "type": "boolean",
            "description": "Live-mode only. When true, the broadcast is archived to a replay VOD after it ends; when false, no archive is produced. Editable while the broadcast is live/upcoming; ignored for VOD-mode broadcasts.\n"
          },
          "visibility": {
            "$ref": "#/components/schemas/BroadcastVisibility"
          },
          "archivePreRollMinutes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 240,
            "nullable": true,
            "description": "Live mode, dedicated stream key only. Minutes before `startsAt` that the live-archive merge window covers. Null (the default) uses the platform default. A broadcast whose stream key is shared with other broadcasts ignores padding entirely, because padding would pull in the neighbouring broadcast's footage. Applies only to an archive that has not run yet; changing it later does not re-create an existing recording. Set through the API only; the admin has no field for it.\n"
          },
          "archivePostRollMinutes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 720,
            "nullable": true,
            "description": "Live mode, dedicated stream key only. Minutes after `endsAt` that the live-archive merge window may cover. The merge ends at whichever comes first, the stream actually stopping or this padded end, so raising it does not extend a replay past the footage. Null (the default) uses the platform default. Raise it for an event that legitimately runs long rather than raising the platform default for everyone. Applies only to an archive that has not run yet; changing it later does not re-create an existing recording.\n"
          },
          "tagIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "collectionItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BroadcastCollectionItemInput"
            }
          }
        }
      },
      "BroadcastCollectionItemInput": {
        "type": "object",
        "required": [
          "collectionItemId"
        ],
        "properties": {
          "collectionItemId": {
            "type": "string",
            "format": "uuid"
          },
          "qualifierId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          }
        }
      },
      "BroadcastDetailResponse": {
        "description": "One broadcast read on its own (`getBroadcast`, `getOrgBroadcast`): the broadcast plus what this caller may do with it.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/BroadcastResponse"
          },
          {
            "type": "object",
            "required": [
              "viewerCanSeeChanges"
            ],
            "properties": {
              "viewerCanSeeChanges": {
                "type": "boolean",
                "description": "Whether the caller may read this broadcast's change history (`listBroadcastChanges`): an admin of its organization, with access to that organization if it is internal. Worked out per caller from the same rule the history endpoint applies, so an admin app can offer the history without asking for it first.\n"
              }
            }
          }
        ]
      },
      "BulkUpsertBroadcastsResponse": {
        "type": "object",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkUpsertResult"
            }
          }
        }
      },
      "BulkUpsertResult": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "ref": {
            "type": "string",
            "description": "Echoed from the matching operation, if provided."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code for this operation (201 created, 200 updated, 400, 403, 404, 409, 422, 500).\n"
          },
          "broadcast": {
            "$ref": "#/components/schemas/BroadcastResponse"
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable error code (mirrors the API's errorCode)."
              },
              "message": {
                "type": "string"
              },
              "invalidFields": {
                "type": "array",
                "description": "Field-level validation errors for this row (e.g. which date field was invalid), surfaced so the caller can flag the offending cell.\n",
                "items": {
                  "$ref": "#/components/schemas/InvalidField"
                }
              }
            }
          }
        }
      },
      "BulkUpsertBroadcastsRequest": {
        "type": "object",
        "required": [
          "operations"
        ],
        "properties": {
          "operations": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/BulkUpsertOperation"
            }
          }
        }
      },
      "BulkUpsertOperation": {
        "type": "object",
        "required": [
          "broadcast"
        ],
        "properties": {
          "ref": {
            "type": "string",
            "description": "Opaque client correlator (e.g. CSV row index) echoed back in the matching result so the caller can map results to source rows.\n"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "When present, update the broadcast with this id (must belong to the target org + hub) instead of creating a new one.\n"
          },
          "broadcast": {
            "$ref": "#/components/schemas/Broadcast"
          }
        }
      },
      "Broadcast": {
        "type": "object",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 72
          },
          "description": {
            "type": "string",
            "nullable": true,
            "maxLength": 240
          },
          "posterUrl": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "streamId": {
            "type": "string",
            "format": "uuid"
          },
          "startsAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "endsAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "maxLength": 64,
            "nullable": true
          },
          "archiveEnabled": {
            "type": "boolean",
            "default": true,
            "description": "Live-mode only. When true (default), the broadcast is archived to a replay VOD after it ends; when false, no archive is produced. Ignored for VOD-mode broadcasts.\n"
          },
          "visibility": {
            "$ref": "#/components/schemas/BroadcastVisibility"
          },
          "archivePreRollMinutes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 240,
            "nullable": true,
            "description": "Live mode, dedicated stream key only. Minutes before `startsAt` that the live-archive merge window covers. Null (the default) uses the platform default. A broadcast whose stream key is shared with other broadcasts ignores padding entirely, because padding would pull in the neighbouring broadcast's footage. Applies only to an archive that has not run yet; changing it later does not re-create an existing recording. Set through the API only; the admin has no field for it.\n"
          },
          "archivePostRollMinutes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 720,
            "nullable": true,
            "description": "Live mode, dedicated stream key only. Minutes after `endsAt` that the live-archive merge window may cover. The merge ends at whichever comes first, the stream actually stopping or this padded end, so raising it does not extend a replay past the footage. Null (the default) uses the platform default. Raise it for an event that legitimately runs long rather than raising the platform default for everyone. Applies only to an archive that has not run yet; changing it later does not re-create an existing recording.\n"
          },
          "tagIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "collectionItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BroadcastCollectionItemInput"
            }
          }
        }
      },
      "BulkUpdateBroadcastsResponse": {
        "type": "object",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkUpdateResult"
            }
          }
        }
      },
      "BulkUpdateResult": {
        "type": "object",
        "required": [
          "broadcastSlug",
          "status"
        ],
        "properties": {
          "broadcastSlug": {
            "type": "string"
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code for this operation (200, 404, 409, 422, 500)"
          },
          "broadcast": {
            "$ref": "#/components/schemas/BroadcastResponse"
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "BulkUpdateBroadcastsRequest": {
        "type": "object",
        "required": [
          "operations"
        ],
        "properties": {
          "operations": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/BulkUpdateOperation"
            }
          }
        }
      },
      "BulkUpdateOperation": {
        "type": "object",
        "required": [
          "broadcastSlug",
          "update"
        ],
        "properties": {
          "broadcastSlug": {
            "type": "string"
          },
          "update": {
            "$ref": "#/components/schemas/BroadcastUpdate"
          }
        }
      },
      "BroadcastList": {
        "type": "object",
        "properties": {
          "broadcasts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BroadcastResponse"
            }
          },
          "total": {
            "type": "integer"
          },
          "paging": {
            "$ref": "#/components/schemas/Paging"
          },
          "facets": {
            "$ref": "#/components/schemas/FacetResults"
          }
        }
      },
      "FacetResults": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "categorySlug": {
              "type": "string"
            },
            "categoryName": {
              "type": "string"
            },
            "values": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "slug": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "count": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      },
      "Paging": {
        "type": "object",
        "properties": {
          "next": {
            "type": "string",
            "nullable": true,
            "description": "Pass as `cursor` to get the next page; null on the last page."
          },
          "hasMore": {
            "type": "boolean",
            "description": "Whether another page follows."
          }
        }
      },
      "BroadcastChangeList": {
        "type": "object",
        "required": [
          "changes",
          "paging"
        ],
        "properties": {
          "changes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BroadcastChange"
            }
          },
          "paging": {
            "$ref": "#/components/schemas/Paging"
          }
        }
      },
      "BroadcastChange": {
        "type": "object",
        "required": [
          "id",
          "action",
          "fields",
          "changedAt",
          "actor"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "action": {
            "type": "string",
            "enum": [
              "created",
              "updated",
              "ended",
              "reopened",
              "streamKeyRotated",
              "deleted"
            ]
          },
          "fields": {
            "type": "array",
            "nullable": true,
            "description": "For `updated`, the fields whose stored value changed; null for every other action. `window` is a made replay's re-trim (its start and end); `schedule` is any other change to a start, end or timezone.\n",
            "items": {
              "type": "string",
              "enum": [
                "title",
                "slug",
                "description",
                "poster",
                "schedule",
                "streamKey",
                "archiving",
                "tags",
                "collectionItems",
                "hub",
                "access",
                "visibility",
                "window"
              ]
            }
          },
          "changedAt": {
            "type": "string",
            "format": "date-time"
          },
          "actor": {
            "$ref": "#/components/schemas/ChangeActor"
          }
        }
      },
      "ChangeActor": {
        "type": "object",
        "description": "Who made a change, and through what. Changes recorded before Eventdoor kept this carry only the username (`attributed` false).\n",
        "required": [
          "name",
          "username",
          "profileId",
          "via",
          "clientId",
          "credentialId",
          "credentialName",
          "attributed"
        ],
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "description": "The person's display name, falling back to the username; null when neither is known.\n"
          },
          "username": {
            "type": "string",
            "nullable": true,
            "description": "The sign-in username recorded with the change."
          },
          "profileId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "via": {
            "type": "string",
            "enum": [
              "direct",
              "assistant",
              "credential"
            ],
            "description": "`assistant` when the change came through this platform's AI assistant (MCP) client, `credential` when made with a scoped API credential, otherwise `direct`.\n"
          },
          "clientId": {
            "type": "string",
            "nullable": true,
            "description": "The OAuth client the change was made through."
          },
          "credentialId": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The scoped API credential, when one was used."
          },
          "credentialName": {
            "type": "string",
            "nullable": true,
            "description": "The credential's name as it is now, when one was used; it stays after the credential is revoked."
          },
          "attributed": {
            "type": "boolean",
            "description": "False for a change recorded before Eventdoor kept who made each change; only `username` is known for it.\n"
          }
        }
      },
      "HubResponse": {
        "type": "object",
        "required": [
          "id",
          "organizationId",
          "name",
          "slug"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "organizationId": {
            "type": "string",
            "format": "uuid"
          },
          "parentId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "avatarUrl": {
            "type": "string",
            "nullable": true
          },
          "bannerUrl": {
            "type": "string",
            "nullable": true
          },
          "posterUrl": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "startsAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "endsAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "nullable": true
          },
          "allDay": {
            "type": "boolean"
          },
          "organization": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "avatarUrl": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GroupedTags"
            }
          },
          "childrenCount": {
            "type": "integer"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HubResponse"
            }
          },
          "gate": {
            "type": "object",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ContentGateSummary"
              }
            ],
            "description": "PUBLIC monetization badge. Null when the hub is free / ungated. Present on list responses; single-hub responses omit it. Carries no viewer entitlement data.\n"
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "updated": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "HubList": {
        "type": "object",
        "properties": {
          "hubs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HubResponse"
            }
          },
          "total": {
            "type": "integer"
          },
          "paging": {
            "$ref": "#/components/schemas/Paging"
          },
          "facets": {
            "$ref": "#/components/schemas/FacetResults"
          }
        }
      }
    },
    "parameters": {
      "broadcastSlugParam": {
        "in": "path",
        "name": "broadcastSlug",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "hubSlugParam": {
        "in": "path",
        "name": "hubSlug",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "orgSlugParam": {
        "in": "path",
        "name": "orgSlug",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "facetsQuery": {
        "name": "facets",
        "in": "query",
        "required": false,
        "description": "Set to \"true\" to include facet counts in the response",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      },
      "cursorQuery": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Opaque cursor for pagination",
        "schema": {
          "type": "string"
        }
      },
      "depthQuery": {
        "name": "depth",
        "in": "query",
        "required": false,
        "description": "Max nesting depth when expand=tree (default 2, max 5)",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 5,
          "default": 2
        }
      },
      "expandQuery": {
        "name": "expand",
        "in": "query",
        "required": false,
        "description": "Set to \"tree\" to include nested children",
        "schema": {
          "type": "string",
          "enum": [
            "tree"
          ]
        }
      },
      "endsBeforeQuery": {
        "name": "endsBefore",
        "in": "query",
        "required": false,
        "description": "Filter to items ending at or before this date-time.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "endsAfterQuery": {
        "name": "endsAfter",
        "in": "query",
        "required": false,
        "description": "Filter to items ending at or after this date-time.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "startsBeforeQuery": {
        "name": "startsBefore",
        "in": "query",
        "required": false,
        "description": "Filter to items starting at or before this date-time.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "startsAfterQuery": {
        "name": "startsAfter",
        "in": "query",
        "required": false,
        "description": "Filter to items starting at or after this date-time.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "hubStateQuery": {
        "name": "state",
        "in": "query",
        "required": false,
        "description": "Filter hubs by temporal state. \"upcoming\" returns hubs whose starts_at is in the future. \"active\" returns hubs currently in progress (starts_at <= now and ends_at is null or in the future). \"past\" returns hubs whose ends_at is in the past.\n",
        "schema": {
          "type": "string",
          "enum": [
            "upcoming",
            "active",
            "past"
          ]
        }
      },
      "parentIdQuery": {
        "name": "parentId",
        "in": "query",
        "required": false,
        "description": "Filter by parent hub ID. Use \"root\" for top-level hubs only.",
        "schema": {
          "type": "string"
        }
      },
      "hubQueryQuery": {
        "name": "query",
        "in": "query",
        "required": false,
        "description": "Search hubs by name",
        "schema": {
          "type": "string",
          "maxLength": 72
        }
      },
      "limitQueryAll": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum number of items to retrieve per page. Use \"all\" to retrieve all items without pagination.\n",
        "schema": {
          "oneOf": [
            {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            {
              "type": "string",
              "enum": [
                "all"
              ]
            }
          ]
        }
      }
    },
    "securitySchemes": {
      "ApiCredential": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "edk_<id>_<secret>",
        "description": "A scoped API credential (`edk_...`), issued in the admin under Settings > Integrations and sent as `Authorization: Bearer edk_...`. Its level, scope and refusals are in [Authentication](https://docs.eventdoor.com/rest-api/authentication); its rate limit and the 429 response are in [Errors](https://docs.eventdoor.com/rest-api/errors#rate-limit)."
      }
    }
  }
}
