# Connect Claude Code (/ai-assistants/claude-code)
Claude Code connects to the same Eventdoor server as Claude on the web and desktop. One connection covers every Eventdoor platform you belong to, and it can only do what you can.
## Add the server [#add-the-server]
Run this command in a terminal:
```bash title="Terminal"
claude mcp add --transport http eventdoor https://mcp.eventdoor.com/eventdoor/api/v1/mcp --client-id eventdoor-assistant --callback-port 43479
```
It registers the server under the name `eventdoor`. The client ID is public and has no client secret.
If the **Connect an assistant** card in your admin (Settings > Integrations) shows a different command, run that one instead.
## Sign in [#sign-in]
In Claude Code, run `/mcp`, choose eventdoor, then Authenticate. Sign in and approve access in the browser window that opens.
The sign-in page is headed "Log in": choose "Log in with Eventdoor", or sign in with Apple or Google. The consent screen reads "Claude access to your Eventdoor platforms". Claude Code never sees your password, and you stay connected without signing in again.
## Why port 43479 [#why-port-43479]
Eventdoor's sign-in accepts Claude Code only on callback port 43479. Keep `--callback-port 43479` exactly as shown: with any other port, or with the flag left out, sign-in is refused with a redirect error.
If you added the server with a different port, remove it and add it again with the command above.
## Share it with your team [#share-it-with-your-team]
`claude mcp add` takes a `--scope` option: `local` (the default, this project for you only), `user` (all your projects), or `project`. With `--scope project`, Claude Code writes the server to a `.mcp.json` file in the project. That file holds no secret, so you can commit it, and each teammate still signs in as themselves.
```bash title="Terminal"
claude mcp add --scope project --transport http eventdoor https://mcp.eventdoor.com/eventdoor/api/v1/mcp --client-id eventdoor-assistant --callback-port 43479
```
## Check it [#check-it]
```bash title="Terminal"
claude mcp list
```
`eventdoor` appears in the list once it is added.
## Remove it [#remove-it]
```bash title="Terminal"
claude mcp remove eventdoor
```
This disconnects Claude Code. Logging out of Eventdoor does not. See [Stay connected and disconnect](/ai-assistants/manage).
# Connect Claude (/ai-assistants)
Connect Claude to Eventdoor, and you can ask it what's live on your platforms or have it edit the draft of a site page for you.
## What it is [#what-it-is]
Each member connects their own assistant once and signs in as themselves. One connection covers every Eventdoor platform they belong to, and it can only do what they can.
Already connected an assistant for another Eventdoor platform? That connection works here too; there's nothing to add.
## Before you start [#before-you-start]
- You need an Eventdoor account that is a member of at least one Eventdoor platform.
- You need Claude on the web or desktop. To use Claude Code instead, see [Connect Claude Code](/ai-assistants/claude-code).
Only Claude (web and desktop) and Claude Code are supported. Other assistants are refused at sign-in.
## Server details [#server-details]
- Server URL: `https://mcp.eventdoor.com/eventdoor/api/v1/mcp`
- Transport: Streamable HTTP
- OAuth client ID: `eventdoor-assistant`
The client ID is public and has no client secret. It is the same for every Eventdoor platform.
If the **Connect an assistant** card in your admin (Settings > Integrations) shows a different server URL, use the values on that card.
## Add the connector [#add-the-connector]
In Claude, open Settings, then Connectors, and choose Add custom connector.
Name it (for example, Eventdoor), and paste the server URL from Server
details above.
Under Advanced settings, paste the client ID from Server details above as
the OAuth Client ID.
Choose Add, then Connect, sign in, and approve access.
On a Team or Enterprise plan, an owner of your Claude organization may need to
add custom connectors before members can use them. See Anthropic's article
[Getting started with custom connectors using remote
MCP](https://support.claude.com/en/articles/11175166-getting-started-with-custom-connectors-using-remote-mcp)
for how. The same article explains how to turn a connector on or off for one
conversation from Claude's **+** menu.
## Signing in [#signing-in]
When you choose Connect, an Eventdoor sign-in page opens, headed "Log in". Choose "Log in with Eventdoor" to sign in with your Eventdoor email and password, or sign in with Apple or Google. The page carries Eventdoor's brand, not your platform's.
After you sign in, a consent screen reads "Claude access to your Eventdoor platforms". Approve it, and Claude acts as you.
- Claude never sees your password.
- You stay connected without signing in again. To disconnect, see [Stay connected and disconnect](/ai-assistants/manage).
## Choosing a platform [#choosing-a-platform]
One connection reaches every Eventdoor platform you belong to. Every tool except `list_platforms` acts on one platform, which the assistant names in a `platform` argument (the platform's id or host).
- If you belong to exactly one platform, there is nothing to choose.
- If you belong to several, ask "list my Eventdoor platforms", or name the platform in your request. When you don't name one, the assistant is told to choose before it acts.
## Try it [#try-it]
**Look something up**
- "What's live now on my platforms?"
- "List the pages I can edit on ``."
**Change a page draft**
- "Add a FAQ section to the home page draft. Show me a dry run first and wait for my OK."
- "Hide the sponsors section on the home page draft. Show me what will change before you save it."
Changes land only in the page's draft, and only when an organization admin has allowed assistants to make changes. A person publishes the draft in the admin. See [What an assistant can do](/ai-assistants/permissions).
## Next [#next]
One command in a terminal.
Every tool, what it reads or changes, and what it can never do.
# Stay connected and disconnect (/ai-assistants/manage)
## Staying connected [#staying-connected]
Once you sign in and approve access, your assistant stays connected without signing in again.
Logging out of Eventdoor does not disconnect the assistant. To stop it, disconnect it as described below.
## Disconnect your assistant [#disconnect-your-assistant]
Remove it from the assistant itself:
- **Claude:** open Settings > Connectors and remove the Eventdoor connector.
- **Claude Code:** run `claude mcp remove eventdoor`.
There is no Eventdoor page for this.
## For admins: connected assistants [#for-admins-connected-assistants]
Admins see who has connected an assistant in Settings > Integrations > **Connected assistants**. There is one row per person, with these columns:
- **Person**
- **Connected**
- **Last active on this platform**: when an assistant last did something on this platform as the member, to within about five minutes. One connection can reach several Eventdoor platforms; use on the others doesn't count here.
Until someone connects, the card reads "No assistants connected yet".
### Disconnect a member's assistant [#disconnect-a-members-assistant]
Open the row's menu and choose **Disconnect**. The dialog asks "Disconnect ``'s assistant?":
> This blocks every assistant they connected on this platform, immediately and in every organization they belong to here, until an admin allows them again. Their assistant keeps working on the other Eventdoor platforms they belong to.
The row then shows **Blocked here**.
### What the blocked person sees [#what-the-blocked-person-sees]
When their assistant tries to act on that platform, it is told:
> An admin on the Eventdoor platform `` disconnected your AI assistant there, so it can't act on that platform. To get access back, ask an admin of your organization on `` to allow you again (Settings > Integrations > Connected assistants). Your other platforms are not affected.
### Allow a member again [#allow-a-member-again]
Open the row's menu and choose **Allow again**. The dialog asks "Allow ``'s assistant again?":
> Their assistant can act on this platform again, in every organization they belong to here. If it stayed signed in, it works right away; otherwise they sign in and approve access again.
## Removing a member [#removing-a-member]
Removing a member from the platform signs them out of Eventdoor and ends their assistant's access to this platform. On other Eventdoor platforms they still belong to, they may need to connect their assistant again.
# What an assistant can do (/ai-assistants/permissions)
A connected assistant works through a fixed set of tools. It can read what you can read, and it can change page drafts only when an organization admin allows it.
## It acts as you [#it-acts-as-you]
The assistant acts as the person who signed in, with the same authorization you have in the admin. It sees nothing you can't see.
The pages it can see are the pages you can edit in the admin. For an organization's pages and its hubs' pages, that means you are an admin of that organization.
## Tools [#tools]
Every tool except `list_platforms` acts on one platform, named in its `platform` argument. See [Choosing a platform](/ai-assistants#choosing-a-platform).
| Tool | What it does | Reads or changes |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `list_platforms` | **List my Eventdoor platforms.** Your platforms, with each one's id, name and host, and whether it is usable now. | Reads |
| `list_my_organizations` | **List my organizations.** The organizations you can work with on this platform. | Reads |
| `get_live_now` | **What's live now.** Broadcasts live right now, with hub, title and scheduled window, for one organization or all of yours. | Reads |
| `list_pages` | **List pages.** The site pages you can edit. A platform admin can read platform pages but not change them. | Reads |
| `get_page` | **Read a page.** A page's live and draft sections, with each section's settings, locked or hidden state, and the page's revision. | Reads |
| `list_section_types` | **List section types.** The section types you can add, as the admin's Add section picker shows them. | Reads |
| `describe_section_type` | **Describe a section type.** A type's settings, defaults and presets. Images take media-library URLs; there are no uploads. | Reads |
| `list_page_versions` | **List page versions.** Published versions, newest first, with who published each and when. | Reads |
| `add_section` | **Add a section.** Adds one section to the page's draft. | **Changes** |
| `update_section` | **Change a section's settings.** | **Changes** |
| `move_section` | **Move a section.** Moves it between its locked neighbours. | **Changes** |
| `set_section_hidden` | **Hide or show a section.** The section keeps its settings. | **Changes** |
| `remove_section` | **Remove a section.** Removes it from the draft. | **Changes** (destructive) |
| `restore_page_version` | **Restore an earlier version.** Brings an earlier published version back as the draft, replacing the current draft. The live page doesn't change. | **Changes** (destructive) |
Read tools are always allowed.
## Changes are off until an admin turns them on [#changes-are-off-until-an-admin-turns-them-on]
The tools that change a page need the organization switch **Let assistants make changes**, in Settings > Integrations, on the Permissions card. It is off by default. The card explains it:
> When off, connected assistants can still view and search this organization's content, but can't create, edit or delete anything. When on, they can edit page drafts; a person still publishes them. Applies to every connection at once.
## Changes only touch drafts [#changes-only-touch-drafts]
- **Drafts only.** A change lands only in the page's draft. Visitors see nothing until a person publishes the draft in the admin. There is no publish tool.
- **Dry run.** Each change tool takes an optional `dry_run` that previews the change without saving it. Ask for one before the assistant saves.
- **Revision check.** Each change names the page and the revision the assistant read. It is refused if the page changed since, so the assistant never overwrites an edit it hasn't seen. Ask it to read the page again and retry.
## What an assistant can't do [#what-an-assistant-cant-do]
- Edit site config: the header, footer or branding.
- Upload media.
- Change platform pages.
- Publish anything.
- Change broadcasts or hubs. It can read them, through What's live now.
Tool results never include stream publish credentials.
## Who made the change [#who-made-the-change]
Each change is credited to you and shown "via AI assistant". The list of published versions shows who published each one.
## Stay safe [#stay-safe]
- **Keep tool approval on.** Have Claude ask before it runs the tools that change a page, so you see each change before it is saved.
- **Be careful combining connectors.** Content that reaches the assistant through Eventdoor or another connector can try to instruct it (prompt injection). Watch what it does when several connectors are on in one conversation.
- **Review the draft before you publish.** The assistant never publishes. Look over the draft in the admin before you do.
# Troubleshooting (/ai-assistants/troubleshooting)
Find the message or symptom you see. In messages, `` is a platform's name and `` an organization's.
## Connecting [#connecting]
Eventdoor accepts only Claude (web and desktop) and Claude Code, and Claude Code only on callback port 43479.
- Connect from Claude or Claude Code. Other assistants are refused at sign-in.
- In Claude Code, run `claude mcp remove eventdoor`, then add it again with the command in [Connect Claude Code](/ai-assistants/claude-code), keeping `--callback-port 43479`.
The full message ends "Ask an admin of your organization to add you." Your Eventdoor account isn't a member of any platform yet. Ask an admin of your organization to add you, then try again.
AI assistant access isn't available on your platform yet. Contact your platform's operator.
This is expected: logging out of Eventdoor does not disconnect the assistant. Disconnect it in Claude or Claude Code as described in [Stay connected and disconnect](/ai-assistants/manage#disconnect-your-assistant).
## Choosing a platform [#choosing-a-platform]
The message goes on "Choose one of: ..." with your platforms. Name the platform in your request, or ask the assistant to list your Eventdoor platforms first.
" isn't one of your Eventdoor platforms`}>
The platform named isn't one you belong to. The message lists the ones you can choose; pick one of those.
(...) did not accept your Eventdoor sign-in"`}>
The message goes on: "you may not be a member there, or that platform doesn't support the shared AI assistant connection yet." Check that you are a member of that platform. If you are, it doesn't support assistants yet.
(...) doesn't offer AI assistant access yet."`}>
That platform doesn't support AI assistants yet. Your other platforms are not affected.
`list_platforms` shows a reason next to it. This is usually brief, so try again in a minute.
## While it works [#while-it-works]
" in time"`}>
The message goes on: "It may still have finished there: check before repeating a change." Ask the assistant to read the page again before it repeats a change, so it doesn't make the change twice.
A passing problem. Try again shortly.
(...) is limiting requests from your assistant. Retry after N seconds."`}>
The assistant sent too many requests in a short time. Wait the number of seconds given, then try again.
disconnected your AI assistant there"`}>
An admin blocked your assistant on that platform. Ask an admin of your organization on that platform to choose **Allow again** in Settings > Integrations > Connected assistants. Your other platforms are not affected.
## Changing pages [#changing-pages]
."`}>
The message goes on: 'An organization admin can turn on "Let assistants make changes" in Settings > Integrations.' Ask an organization admin to turn it on. This also applies to dry runs. Reading is always allowed. See [Changes are off until an admin turns them on](/ai-assistants/permissions#changes-are-off-until-an-admin-turns-them-on).
An assistant can read platform pages but never change them. Make the change in the admin.
since you read it." or "The live page changed since you read it (someone published it)."`}>
Someone else edited or published the page after the assistant read it, so the change was refused rather than overwrite theirs. Ask the assistant to read the page again, then make the change.
" that you can edit..." or "No organization "" whose pages you can edit..."`}>
You aren't an admin of the organization that owns that page, or the page no longer exists. Ask the assistant to list the pages you can edit.
# Eventdoor developer docs (/)
Eventdoor runs your live and on-demand event platform. These docs cover the two ways to work with it from outside the admin.
## Connect an AI assistant [#connect-an-ai-assistant]
Add Eventdoor to Claude on the web or desktop as a custom connector.
One command in a terminal.
It acts as you, reads what you can read, and edits page drafts only when an
organization admin allows it.
## Call the REST API [#call-the-rest-api]
Issue a credential and make your first call.
Read or write, one organization or the whole platform, and when a credential
expires.
Every operation a credential can call: hubs and broadcasts.
There are no webhooks or SDKs; everything else is done in the Eventdoor admin.
## AI resources [#ai-resources]
These docs are written to be read by AI agents and coding assistants as well as people.
- [/llms.txt](/llms.txt) lists every page with a one-line summary.
- [/llms-full.txt](/llms-full.txt) has every page as Markdown in one file.
- Every page has a `.md` version: add `.md` to its address (this page is [/index.md](/index.md)). **Copy Markdown** at the top of each page copies it.
- The OpenAPI spec for the REST API is at [/rest-api/openapi.json](/rest-api/openapi.json).
- To let an assistant work on your Eventdoor platforms, not just read these docs, [connect it over MCP](/ai-assistants).
# Authentication (/rest-api/authentication)
Every REST API request carries a scoped API credential. You issue credentials in the admin; there is no other way to get one.
## Send the token [#send-the-token]
Send the token as a bearer token on every request:
```text title="Request header"
Authorization: Bearer edk_...
```
A token has the form `edk__`. Send the whole string.
```bash title="An authenticated request"
curl -s -H "Authorization: Bearer $EVENTDOOR_TOKEN" \
"$EVENTDOOR_API/organizations/acme/hubs"
```
The credential is checked on every request before anything else. A token in the `edk_...` form that does not authenticate gets `401`, even on an operation that would otherwise be public. A bearer token that does not start with `edk_` (lowercase) is not treated as a credential: on a public read it is served as an anonymous request, with no error, and returns only what anyone may see. See [Errors and rate limits](/rest-api/errors).
## Access levels [#access-levels]
When you issue a credential, you choose its **Access**:
| Access | In the API | What it may do |
| ------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Read only** | `read` | GET requests only, acting as a member of the organization. Can view what a member of this organization can view. Changes nothing. |
| **Read and write** | `write` | Acts as an admin of the organization. Never as an owner, and can't manage members or credentials. |
A **Read only** credential cannot [read a broadcast's change history](/rest-api/reference/list-broadcast-changes); that needs **Read and write**.
No credential ever acts as an owner or a system admin, and none unlocks paid content.
## Scope: one organization or the whole platform [#scope-one-organization-or-the-whole-platform]
- **An organization's credential** reaches that organization only. A request for any other organization is refused with `403`.
- **A platform-wide credential** reaches every organization on the platform except internal ones. A **Read and write** one acts as an admin in each.
## A credential is not a user [#a-credential-is-not-a-user]
A credential is not a user: it has no profile, is not a member, and keeps working when the person who issued it leaves. The changes it makes name the credential.
## Who can issue a credential [#who-can-issue-a-credential]
- **For an organization**: its owners and admins. In the admin sidebar, open **Settings**, then the **Integrations** tab, and find **API credentials**. Only the organization's owners and admins, and platform admins, can see and revoke its credentials.
- **Platform-wide**: platform admins, from **Settings > Integrations** on the platform itself.
The [getting started guide](/rest-api) walks through the **Issue credential** dialog.
## Expiry [#expiry]
No credential lasts forever. Every one expires after the period you choose: **In 30 days**, **In 90 days** or **In 1 year**. The default is 1 year. After it expires, its requests get `401 This API credential has expired`; issue a new one.
The **API credentials** table shows each credential's **Name**, **Status** (Active, Expired or Revoked), **Access**, **Token** (its first characters only), **Created**, **Expires** and **Last used**.
## Revoking [#revoking]
To stop a credential, open its row's menu in the **API credentials** table and choose **Revoke**. The confirmation explains what happens:
> Anything using this token stops working at once: its next request is refused. The credential stays in the list as revoked, and the changes it made still name it. This can't be undone.
## Keep the token secret [#keep-the-token-secret]
The token is shown once, when you issue it. Only a fingerprint of it is stored, so it cannot be shown again. If it's lost, revoke the credential and issue a new one.
- Keep the token on your server or in a secret store, such as a password manager or your tool's secret settings.
- Never ship it in a browser or a mobile app.
- Issue one credential per tool, with the least access it needs, so you can revoke one without breaking the others.
# Bulk operations (/rest-api/bulk)
Two calls change many broadcasts in one hub at once. Both need a **Read and write** credential.
| Call | Path | What it does |
| -------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------- |
| [Bulk update broadcasts](/rest-api/reference/bulk-update-broadcasts) | `PATCH /organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/bulk` | Update existing broadcasts by slug |
| [Bulk upsert broadcasts](/rest-api/reference/bulk-upsert-broadcasts) | `POST /organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/bulk-upsert` | Create broadcasts, or update by id |
## Limits [#limits]
- A bulk call carries 1 to 50 entries in `operations`. More than 50 is refused with `400 A bulk call may carry at most 50 operations`. Split a larger batch into several calls.
- A bulk call costs one request per entry in `operations` against your [rate limit](/rest-api/errors#rate-limit). A call with 50 operations costs 50.
## Results: 207, one per row [#results-207-one-per-row]
Both calls answer `207` with a `results` array, one result per entry in `operations`. Each row succeeds or fails on its own: one bad row does not stop the others, and the call still returns `207`. Always check each result's `status`.
A `startsAt` you set must be in the future. A row with a past `startsAt` fails with `422`.
## Bulk update [#bulk-update]
Each operation names a broadcast by `broadcastSlug` and carries an `update` with the fields to change, the same fields as [Update broadcast](/rest-api/reference/update-broadcast).
```bash title="Bulk update"
curl -s -X PATCH \
-H "Authorization: Bearer $EVENTDOOR_TOKEN" \
-H "Content-Type: application/json" \
"$EVENTDOOR_API/organizations/acme/hubs/home-games/broadcasts/bulk" \
-d '{
"operations": [
{ "broadcastSlug": "vs-celtics", "update": { "startsAt": "2027-03-14T19:30:00Z" } },
{ "broadcastSlug": "vs-knicks", "update": { "startsAt": "2027-03-21T19:30:00Z" } }
]
}'
```
Each result has the `broadcastSlug`, a `status`, and either the updated `broadcast` or an `error`:
| `status` | Meaning | `error.code` |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `200` | Updated. `broadcast` holds the new version. | |
| `404` | No broadcast with that slug in this hub. | `NOT_FOUND` |
| `409` | The change conflicts with the current state. | `CONFLICT` |
| `422` | The update is not valid. | `VALIDATION_ERROR` |
| `500` | A server error on this row, or an id in `update` that names nothing this organization has, such as an unknown `graphicsTemplateId`. Check the ids before you retry. | `INTERNAL_ERROR` |
An illustrative response, where the second row failed:
```json title="207 response"
{
"results": [
{
"broadcastSlug": "vs-celtics",
"status": 200,
"broadcast": {
"slug": "vs-celtics",
"title": "vs Celtics",
"startsAt": "2027-03-14T19:30:00Z"
}
},
{
"broadcastSlug": "vs-knicks",
"status": 404,
"error": { "code": "NOT_FOUND", "message": "Broadcast not found" }
}
]
}
```
## Bulk upsert [#bulk-upsert]
Each operation carries a `broadcast` in the shape [Create broadcast](/rest-api/reference/create-broadcast) takes, so `title` is required. Bulk upsert applies only `title`, `description`, `posterUrl`, `startsAt`, `endsAt`, `timezone`, `collectionItems` and `tagIds`, plus `streamId` when it creates a broadcast. It ignores the archive fields, and `streamId` on an update. On an update, `collectionItems` are added to the broadcast's existing collection links rather than replacing them.
Two optional fields go with the `broadcast`:
- `id`: update the broadcast with this id, which must be in this hub, instead of creating a new one.
- `ref`: any string of yours, such as a spreadsheet row number. It comes back on the matching result so you can map results to your rows.
```bash title="Bulk upsert"
curl -s -X POST \
-H "Authorization: Bearer $EVENTDOOR_TOKEN" \
-H "Content-Type: application/json" \
"$EVENTDOOR_API/organizations/acme/hubs/home-games/broadcasts/bulk-upsert" \
-d '{
"operations": [
{ "ref": "row-2", "broadcast": { "title": "vs Celtics", "startsAt": "2027-03-14T19:30:00Z" } },
{ "ref": "row-3", "id": "3b9d6c1e-7a42-4f0e-8c15-2e6a9f0d4b71", "broadcast": { "title": "vs Knicks" } }
]
}'
```
Each result has your `ref`, a `status`, and either the `broadcast` or an `error` with `code`, `message` and sometimes `invalidFields`. A `status` of `201` means the broadcast was created and `200` that it was updated. A failed row has `400`, `403`, `404`, `409`, `422` or `500`.
An illustrative response, where the first row was created and the second row's `id` matched no broadcast in this hub:
```json title="207 response"
{
"results": [
{
"ref": "row-2",
"status": 201,
"broadcast": {
"slug": "vs-celtics",
"title": "vs Celtics",
"startsAt": "2027-03-14T19:30:00Z"
}
},
{
"ref": "row-3",
"status": 404,
"error": { "code": "NOT_FOUND", "message": "Broadcast not found" }
}
]
}
```
Fix the failed rows and send only those again. With `id` set on a row, sending it again updates that broadcast rather than creating a second one.
A row without `id` that fails with `400` on `tagIds` has still created its broadcast: the tags are checked after the broadcast is saved, and the result carries no id. Before you send that row again, [list the hub's broadcasts](/rest-api/reference/list-broadcasts) and look for its title. If it is there, send the row with that broadcast's `id` instead, or it is created a second time.
# Errors and rate limits (/rest-api/errors)
When the API refuses a request, it answers with an HTTP status and a JSON body that says why. This page lists the refusals a credential can get and explains the rate limit.
## The error body [#the-error-body]
Every error body is JSON with the reason in `message`. Some errors add more fields:
| Field | Type | Description |
| --------------- | -------- | ------------------------------------------------ |
| `message` | `string` | Why the request was refused. Always present. |
| `errorCode` | `string` | A machine-readable code, such as `rate_limited`. |
| `invalidFields` | `array` | The request fields that failed validation. |
| `reference` | `string` | A support id. Quote it when you contact support. |
```json title="Example error body"
{ "message": "This API credential has expired" }
```
## Credential refusals [#credential-refusals]
| Status | Message | What to do |
| ------ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 401 | `Invalid API credential` | The token starts with `edk_` but is malformed or unknown. Check that you send the whole token, then issue a new credential if it still fails. A token without the `edk_` prefix gets no `401` on a public read: it is served anonymously. |
| 401 | `This API credential has been revoked` | Someone revoked it. Issue a new credential. |
| 401 | `This API credential has expired` | Issue a new credential. |
| 403 | `This operation does not accept API credentials` | Use only the operations in the [API reference](/rest-api/reference). |
| 403 | `This API credential is read-only: it may make GET requests only` | Use a **Read and write** credential for this request. |
| 403 | `This API credential is scoped to the organization ` | The path names another organization. Use that organization's slug, or a credential for that organization. |
| 403 | `This API credential is scoped to the organization ; this operation is not` | The operation names no organization. Call an operation under `/organizations/{orgSlug}`. |
| 403 | `A read-only API credential cannot read change history` | [Reading a broadcast's change history](/rest-api/reference/list-broadcast-changes) needs a **Read and write** credential. |
| 400 | `A bulk call may carry at most 50 operations` | Split the batch into calls of 50 or fewer. See [Bulk operations](/rest-api/bulk). |
The credential is checked on every request before anything else. A token in the `edk_...` form that does not authenticate gets `401`, even on an operation that would otherwise be public. A bearer token that does not start with `edk_` (lowercase) is not treated as a credential: on a public read it is served as an anonymous request, with no error.
### The 401 header [#the-401-header]
Every `401` carries this header:
```text title="Response header"
WWW-Authenticate: Bearer error="invalid_token"
```
## Other statuses [#other-statuses]
- Every operation can return `404` (not found), `429` (rate limited, below) and `500` (server error).
- Write operations can also return `409` when the change conflicts with the current state.
- Write operations can also return `422` when a field breaks a rule, for example a `startsAt` in the past or an `endsAt` before `startsAt`. The body carries `errorCode` `SCHEDULE_INVALID` and `invalidFields`.
- A create or update can also return `400` when it names something that does not exist or is not available to this organization: an unknown `graphicsTemplateId` on an update (the body carries `invalidFields`), or a `tagIds` entry on a create or update. A create that returns `400` for `tagIds` has already created the broadcast, so look it up before retrying.
- Bulk calls return `207` with one result per row. See [Bulk operations](/rest-api/bulk).
If a `500` keeps happening, contact support and quote the `reference` from the error body when there is one.
## Rate limit [#rate-limit]
Each credential has its own rate limit: a burst of requests it can make at once, which refills over time.
By default a credential can make a burst of about 60 requests, refilling at about 120 per minute. Your platform's operator can change these, and you may occasionally be limited a little sooner or later than that, so always handle `429` rather than counting on an exact number.
### What a request costs [#what-a-request-costs]
- A [bulk call](/rest-api/bulk) costs one request per entry in `operations`.
- Every other call costs one request, including a list read with `limit=all`.
### The 429 response [#the-429-response]
Over the rate limit, a request gets `429` with a `Retry-After` header, in seconds, and this body:
```json title="429 body"
{
"message": "Too many requests from this API credential. Retry after 3 seconds.",
"errorCode": "rate_limited",
"retryAfterSeconds": 3
}
```
Wait that many seconds, then retry.
### Retry after a rate limit [#retry-after-a-rate-limit]
```bash title="Retry on 429"
url="$EVENTDOOR_API/organizations/acme/hubs/home-games/broadcasts"
for attempt in 1 2 3 4 5; do
status=$(curl -s -D headers.txt -o body.json -w '%{http_code}' \
-H "Authorization: Bearer $EVENTDOOR_TOKEN" "$url")
[ "$status" != "429" ] && break
wait=$(grep -i '^retry-after:' headers.txt | tr -dc '0-9')
sleep "${wait:-1}"
done
cat body.json
```
```js title="Retry on 429"
async function eventdoor(path, init = {}, attempts = 5) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(`${process.env.EVENTDOOR_API}${path}`, {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${process.env.EVENTDOOR_TOKEN}`,
},
});
if (res.status !== 429 || attempt === attempts) return res;
const seconds = Number(res.headers.get("Retry-After")) || 1;
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
}
const res = await eventdoor("/organizations/acme/hubs/home-games/broadcasts");
```
Retry only the request that got `429`. Retrying a `4xx` other than `429` without changing anything gets the same refusal.
# Get started with the REST API (/rest-api)
The Eventdoor REST API lets your own scripts and tools work with the hubs and broadcasts of your organizations. You call it with a scoped API credential that you issue in the admin.
A credential can call 12 operations: list and read hubs, and list, read, create, update and bulk-change broadcasts. The [API reference](/rest-api/reference) lists every operation that accepts a credential; no other operation does.
## How the API is organized [#how-the-api-is-organized]
```text
Platform
└── Organization /organizations/{orgSlug}
├── Broadcast /organizations/{orgSlug}/broadcasts/{broadcastSlug}
└── Hub /organizations/{orgSlug}/hubs/{hubSlug}
└── Broadcast /organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/{broadcastSlug}
```
Every path starts with `/organizations/{orgSlug}`. Paths scoped to a hub add `/hubs/{hubSlug}`.
## Before you start [#before-you-start]
- You must be an owner or admin of the organization to issue its credential. A platform-wide credential is issued by a platform admin. See [Who can issue a credential](/rest-api/authentication#who-can-issue-a-credential).
- You need your organization's slug. It is the first part of the admin's address while that organization is selected: in `/acme/settings/integrations`, the slug is `acme`. An organization's page in the admin also shows it as **Slug** under **Details**.
## Make your first call [#make-your-first-call]
### Issue a credential [#issue-a-credential]
In the admin sidebar, open **Settings**, then the **Integrations** tab. Under **API credentials**, choose **Issue credential**.
In the **Issue an API credential** dialog, fill in three fields:
- **Name**: what the credential is for, for example "Nightly schedule sync". Up to 100 characters.
- **Access**: **Read only** or **Read and write**. See [Authentication](/rest-api/authentication) for what each allows.
- **Expires**: **In 30 days**, **In 90 days** or **In 1 year**. The default is 1 year.
Next, **Copy your new token** shows the token. Choose **Copy token**, store it somewhere safe, then choose **Done**. You won't see this token again.
### Copy your API base URL [#copy-your-api-base-url]
The same **API credentials** section has an **API base URL** field with a copy button. It looks like this:
```text title="Base URL"
https:///eventdoor/api/v1
```
Put the base URL and your token in environment variables so the examples below work as written:
```bash title="Terminal"
export EVENTDOOR_API="https:///eventdoor/api/v1"
export EVENTDOOR_TOKEN=""
```
### Call the API [#call-the-api]
[List the hubs](/rest-api/reference/list-org-hubs) of your organization. Replace `acme` with your organization's slug.
```bash title="List hubs"
curl -s -H "Authorization: Bearer $EVENTDOOR_TOKEN" \
"$EVENTDOOR_API/organizations/acme/hubs"
```
```js title="List hubs"
const res = await fetch(
`${process.env.EVENTDOOR_API}/organizations/acme/hubs`,
{
headers: { Authorization: `Bearer ${process.env.EVENTDOOR_TOKEN}` },
},
);
const body = await res.json();
```
### Read the JSON [#read-the-json]
The response lists the hubs, the total count and the paging state. The values here are illustrative:
```json title="Response"
{
"hubs": [
{
"id": "6f1c2a9e-0b7d-4c1e-9a52-3d8e4f7a1b20",
"slug": "home-games",
"name": "Home games"
}
],
"total": 1,
"paging": { "next": null, "hasMore": false }
}
```
A real hub carries more fields; the [reference](/rest-api/reference/list-org-hubs) lists them all. When `paging.hasMore` is `true`, read the next page as [Pagination](/rest-api/pagination) shows.
## Next steps [#next-steps]
### List the broadcasts in a hub [#list-the-broadcasts-in-a-hub]
[List broadcasts in a hub](/rest-api/reference/list-broadcasts):
```bash title="List broadcasts"
curl -s -H "Authorization: Bearer $EVENTDOOR_TOKEN" \
"$EVENTDOOR_API/organizations/acme/hubs/home-games/broadcasts"
```
```js title="List broadcasts"
const res = await fetch(
`${process.env.EVENTDOOR_API}/organizations/acme/hubs/home-games/broadcasts`,
{ headers: { Authorization: `Bearer ${process.env.EVENTDOOR_TOKEN}` } },
);
const { broadcasts } = await res.json();
```
### Create a broadcast [#create-a-broadcast]
[Create a broadcast](/rest-api/reference/create-broadcast) with a **Read and write** credential. `title` is required. Keep it to 72 characters: a longer title is not refused, but only its first 72 characters are stored. A `startsAt`, if you send one, must be in the future, or the call is refused with `422`. A new broadcast returns `201`. A `400` on `tagIds` can arrive after the broadcast was created, since the tags are checked once it is saved: list the hub's broadcasts and look for the title before you create it again.
```bash title="Create a broadcast"
curl -s -X POST \
-H "Authorization: Bearer $EVENTDOOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"vs Celtics"}' \
"$EVENTDOOR_API/organizations/acme/hubs/home-games/broadcasts"
```
```js title="Create a broadcast"
const res = await fetch(
`${process.env.EVENTDOOR_API}/organizations/acme/hubs/home-games/broadcasts`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EVENTDOOR_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ title: "vs Celtics" }),
},
);
const broadcast = await res.json();
```
### Learn the rules [#learn-the-rules]
- [Authentication](/rest-api/authentication): what each access level may do, scope, expiry and revoking.
- [Errors and rate limits](/rest-api/errors): what each refusal means and how to retry.
Store the token in a secret store or your tool's secret settings. Never ship
it in a browser or a mobile app: anyone who has it can call the API with its
access until it expires or you revoke it.
## Your platform's interactive reference [#your-platforms-interactive-reference]
In the admin, **Settings > Integrations** has an **API reference** button. It opens your platform's own interactive copy of the reference, where **Try it** calls your platform directly.
# Pagination (/rest-api/pagination)
List operations return one page at a time. You choose the page size with `limit` and move to the next page with `cursor`.
## Query parameters [#query-parameters]
| Parameter | Type | Default | Description |
| --------- | ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `limit` | `integer \| "all"` | `20` | Items per page: 1-100, or `all` for the whole list in one response. An integer out of range is clamped to 1-100, not refused, except `0`: `0` or a value that is not a number means the default, 20. |
| `cursor` | `string` | | The `paging.next` value from the previous page. Leave it out for the first page. |
## The response [#the-response]
A list response holds the items, the total count and the paging state:
```json title="A page of broadcasts"
{
"broadcasts": [ ... ],
"total": 134,
"paging": { "next": "eyJvIjoyMH0", "hasMore": true }
}
```
- The list key is `hubs`, `broadcasts` or `changes`, depending on the operation.
- `total` is the number of items across all pages.
- `paging.next` is the cursor for the next page, and `null` on the last page.
- `paging.hasMore` says whether another page follows.
The change history has no `total`; see [The change history is different](#the-change-history-is-different).
To read the next page, pass `paging.next` back as `?cursor=`, with the same filters. Treat the cursor as opaque: don't build or change one yourself. On a hub or broadcast list, a cursor the API cannot read returns the first page.
## Read every page [#read-every-page]
This loop reads every broadcast in a hub, 100 at a time.
```bash title="Read every page (needs jq)"
cursor=""
while :; do
args=(-s -G -H "Authorization: Bearer $EVENTDOOR_TOKEN" --data-urlencode "limit=100")
[ -n "$cursor" ] && args+=(--data-urlencode "cursor=$cursor")
page=$(curl "${args[@]}" "$EVENTDOOR_API/organizations/acme/hubs/home-games/broadcasts")
echo "$page" | jq -r '.broadcasts[].slug'
[ "$(echo "$page" | jq -r '.paging.hasMore')" = "true" ] || break
cursor=$(echo "$page" | jq -r '.paging.next')
done
```
```js title="Read every page"
const base = `${process.env.EVENTDOOR_API}/organizations/acme/hubs/home-games/broadcasts`;
const headers = { Authorization: `Bearer ${process.env.EVENTDOOR_TOKEN}` };
const all = [];
let cursor = null;
do {
const url = new URL(base);
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers });
const { broadcasts, paging } = await res.json();
all.push(...broadcasts);
cursor = paging.hasMore ? paging.next : null;
} while (cursor);
```
Each page is one request against your [rate limit](/rest-api/errors#rate-limit). `limit=all` returns the whole list in one request, which also costs one.
## Filters [#filters]
Filters narrow a list before it is paged. Keep the same filters on every page of one read.
### Broadcast lists [#broadcast-lists]
[List broadcasts in a hub](/rest-api/reference/list-broadcasts) and [list an organization's broadcasts](/rest-api/reference/list-org-broadcasts) take:
| Parameter | Values |
| -------------------------------------------------------- | ----------------------------------------------------------------------- |
| `state` | `upcoming`, `active`, `live` or `past`. `live` is not paged: see below. |
| `startsAfter`, `startsBefore`, `endsAfter`, `endsBefore` | A date-time, for example `2026-03-01T00:00:00Z` |
| `tag`, `tagCategory` | A tag or tag-category slug |
| `collection`, `collectionCategory` | A collection or collection-category slug |
| `collectionItem` | A collection item id |
| `mode` | `live` or `vod` |
`state=live` returns at most `limit` broadcasts in one response, with `paging.hasMore` `false` and no `next`, even when more are live. `total` counts every live broadcast. Pass `limit=all` to get them all, or compare `total` with the number returned.
```bash title="Upcoming broadcasts in a hub"
curl -s -G -H "Authorization: Bearer $EVENTDOOR_TOKEN" \
--data-urlencode "state=upcoming" \
"$EVENTDOOR_API/organizations/acme/hubs/home-games/broadcasts"
```
### Hub lists [#hub-lists]
[List hubs](/rest-api/reference/list-org-hubs) takes:
| Parameter | Values |
| -------------------------------------------------------------------------- | ---------------------------------------------------- |
| `query` | Text to search hub names for |
| `parentId` | A parent hub's id, or `root` for top-level hubs only |
| `state` | `upcoming`, `active` or `past` |
| `startsAfter`, `startsBefore`, `endsAfter`, `endsBefore` | A date-time |
| `tag`, `tagCategory`, `collection`, `collectionCategory`, `collectionItem` | As for broadcasts |
### Filter by tag category [#filter-by-tag-category]
All three lists also filter by tag: any other query parameter is read as a tag-category slug, and its value as a comma-separated list of tag slugs in that category. Within one category a row matches any of the tags; across categories it must match every category. For example, `?age_group=5-8,9-12&sport=swimming` returns rows tagged `5-8` or `9-12` in `age_group` and also tagged `swimming` in `sport`.
Because an unrecognised parameter is read as a tag category, a misspelt filter returns an empty list rather than an error.
With at least one tag-category filter, add `facets=true` to get a `facets` array next to `paging`: for each category you filter on, its tags and how many rows would match each tag under your other category filters.
## The change history is different [#the-change-history-is-different]
[Listing a broadcast's changes](/rest-api/reference/list-broadcast-changes) pages the same way, with `cursor` and `paging`, with four differences:
- Its `limit` is 1-50, default 20. It has no `all`.
- An integer `limit` out of range, including `0`, is clamped to 1-50. Anything else, including `all`, means 20, with no error.
- A cursor it cannot read is refused with `400 Invalid cursor` instead of returning the first page.
- Its response has `changes` and `paging` only, with no `total`.
# API reference (/rest-api/reference)
This reference lists every operation that accepts a scoped API credential; no other operation does.
## Base URL and authentication [#base-url-and-authentication]
Every path below is relative to your platform's API base URL:
```
https:///eventdoor/api/v1
```
Copy yours from **Settings > Integrations** in the admin.
Send your credential as a bearer token on every request:
```
Authorization: Bearer edk_...
```
See [Authentication](/rest-api/authentication) for issuing a credential and what each level may do.
## Operations [#operations]
| Method | Path | What it does |
| ------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `GET` | [`/organizations/{orgSlug}/hubs`](/rest-api/reference/list-org-hubs) | List organization hubs |
| `GET` | [`/organizations/{orgSlug}/hubs/{hubSlug}`](/rest-api/reference/get-hub) | Get hub (`expand=tree`, `depth` 1-5, default 2) |
| `GET` | [`/organizations/{orgSlug}/broadcasts`](/rest-api/reference/list-org-broadcasts) | List broadcasts (org-wide) |
| `GET` | [`/organizations/{orgSlug}/broadcasts/{broadcastSlug}`](/rest-api/reference/get-org-broadcast) | Get broadcast (org-scoped) |
| `PATCH` | [`/organizations/{orgSlug}/broadcasts/{broadcastSlug}`](/rest-api/reference/update-org-broadcast) | Update broadcast (org-scoped) |
| `GET` | [`/organizations/{orgSlug}/broadcasts/{broadcastSlug}/changes`](/rest-api/reference/list-broadcast-changes) | List who changed a broadcast, newest first |
| `GET` | [`/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts`](/rest-api/reference/list-broadcasts) | List broadcasts in a hub |
| `POST` | [`/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts`](/rest-api/reference/create-broadcast) | Create broadcast (201; `title` required, stored up to 72 characters) |
| `PATCH` | [`/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/bulk`](/rest-api/reference/bulk-update-broadcasts) | Bulk update broadcasts (207) |
| `POST` | [`/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/bulk-upsert`](/rest-api/reference/bulk-upsert-broadcasts) | Bulk upsert broadcasts (207) |
| `GET` | [`/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/{broadcastSlug}`](/rest-api/reference/get-broadcast) | Get broadcast |
| `PATCH` | [`/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/{broadcastSlug}`](/rest-api/reference/update-broadcast) | Update broadcast |
## Related guides [#related-guides]
- [Authentication](/rest-api/authentication): issuing a credential, levels and scope.
- [Errors](/rest-api/errors): what each refusal means.
- [Pagination](/rest-api/pagination): reading a list page by page.
- [Bulk operations](/rest-api/bulk): updating or upserting many broadcasts in one call.
## The spec [#the-spec]
[Download the OpenAPI spec](/rest-api/openapi.json) to generate a client or import it into your API tool.
Your platform's admin (**Settings > Integrations > API reference**) opens an interactive copy of this reference, with **Try it** against your own platform.
# List organization hubs
`GET /organizations/{orgSlug}/hubs`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "GET",
"path": "/organizations/{orgSlug}/hubs",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"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"
]
}
]
}
},
"cursorQuery": {
"name": "cursor",
"in": "query",
"required": false,
"description": "Opaque cursor for pagination",
"schema": {
"type": "string"
}
},
"hubQueryQuery": {
"name": "query",
"in": "query",
"required": false,
"description": "Search hubs by name",
"schema": {
"type": "string",
"maxLength": 72
}
},
"parentIdQuery": {
"name": "parentId",
"in": "query",
"required": false,
"description": "Filter by parent hub ID. Use \"root\" for top-level hubs only.",
"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"
]
}
},
"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"
]
}
},
"startsAfterQuery": {
"name": "startsAfter",
"in": "query",
"required": false,
"description": "Filter to items starting 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"
}
},
"endsAfterQuery": {
"name": "endsAfter",
"in": "query",
"required": false,
"description": "Filter to items ending at or after this date-time.",
"schema": {
"type": "string",
"format": "date-time"
}
},
"endsBeforeQuery": {
"name": "endsBefore",
"in": "query",
"required": false,
"description": "Filter to items ending at or before this date-time.",
"schema": {
"type": "string",
"format": "date-time"
}
}
},
"schemas": {
"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"
}
}
},
"HubResponse": {
"type": "object",
"required": [
"id",
"organizationId",
"name",
"slug"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"parentId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"allDay": {
"type": "boolean"
},
"organization": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"tags": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GroupedTags"
}
},
"childrenCount": {
"type": "integer"
},
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/HubResponse"
}
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
},
"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"
}
}
}
}
}
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"Paging": {
"type": "object",
"properties": {
"next": {
"type": [
"string",
"null"
],
"description": "Pass as `cursor` to get the next page; null on the last page."
},
"hasMore": {
"type": "boolean",
"description": "Whether another page follows."
}
}
},
"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"
}
}
}
}
}
}
},
"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"
}
}
}
},
"responses": {
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# Get hub
`GET /organizations/{orgSlug}/hubs/{hubSlug}`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "GET",
"path": "/organizations/{orgSlug}/hubs/{hubSlug}",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
},
{
"$ref": "#/components/parameters/hubSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"hubSlugParam": {
"in": "path",
"name": "hubSlug",
"required": true,
"schema": {
"type": "string"
}
},
"expandQuery": {
"name": "expand",
"in": "query",
"required": false,
"description": "Set to \"tree\" to include nested children",
"schema": {
"type": "string",
"enum": [
"tree"
]
}
},
"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
}
}
},
"schemas": {
"HubResponse": {
"type": "object",
"required": [
"id",
"organizationId",
"name",
"slug"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"parentId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"allDay": {
"type": "boolean"
},
"organization": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"tags": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GroupedTags"
}
},
"childrenCount": {
"type": "integer"
},
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/HubResponse"
}
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
},
"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"
}
}
}
}
}
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"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"
}
}
}
},
"responses": {
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# List broadcasts (org-wide)
`GET /organizations/{orgSlug}/broadcasts`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "GET",
"path": "/organizations/{orgSlug}/broadcasts",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"cursorQuery": {
"name": "cursor",
"in": "query",
"required": false,
"description": "Opaque cursor for pagination",
"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"
]
}
}
},
"schemas": {
"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"
}
}
},
"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",
"null"
],
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"vodId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "ID of the attached VOD, or null when the broadcast is live."
},
"attachedVod": {
"$ref": "#/components/schemas/AttachedVodSummary"
},
"clipStartsAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"description": "The user who force-ended this broadcast, or null."
},
"endReason": {
"type": [
"string",
"null"
],
"description": "Operator-supplied note explaining the force-end, or null."
},
"archiveAttemptedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "uuid",
"description": "The auto-created default program for this broadcast. Null for VOD-mode broadcasts."
},
"activeProgramId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
]
}
}
},
"title": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"description": "The EFFECTIVE poster: a manual poster wins over a template-generated one; null falls through to the surface placeholder."
},
"generatedPosterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "uuid",
"description": "The graphics template applied to this broadcast, or null."
},
"useGeneratedPoster": {
"type": "boolean",
"description": "Per-broadcast poster-generation toggle."
},
"streamId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Null for VOD-mode broadcasts."
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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",
"null"
]
},
"defaultStreamPublicKey": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"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",
"null"
],
"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"
}
}
},
"AttachedVodSummary": {
"type": [
"object",
"null"
],
"required": [
"id",
"status",
"originalArchiveAvailable"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"status": {
"type": "string",
"enum": [
"pending",
"queued",
"processing",
"ready",
"failed"
]
},
"videoUrl": {
"type": [
"string",
"null"
],
"description": "Present when status is \"ready\""
},
"videoDuration": {
"type": [
"integer",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"description": "Original filename of the source upload."
},
"xcodeUploadId": {
"type": [
"integer",
"null"
],
"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",
"null"
],
"format": "date-time",
"description": "When the original live capture was replaced, or null when none is stashed."
}
}
},
"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."
},
"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",
"null"
]
},
"previewManifestUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"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",
"null"
]
},
"webrtc": {
"type": [
"string",
"null"
]
}
}
},
"transcode": {
"type": "boolean"
},
"authType": {
"type": [
"string",
"null"
]
},
"appData": {
"type": [
"string",
"null"
]
},
"rtmpPush": {
"type": [
"string",
"null"
],
"description": "JSON-encoded RTMP push configuration"
},
"mode": {
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}
}
}
},
"BroadcastCollectionItemResponse": {
"allOf": [
{
"$ref": "#/components/schemas/CollectionItemResponse"
},
{
"type": "object",
"properties": {
"qualifier": {
"type": [
"object",
"null"
],
"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",
"null"
],
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
}
}
},
"type": {
"type": [
"object",
"null"
],
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"system": {
"type": "boolean"
},
"imageStyle": {
"$ref": "#/components/schemas/ImageStyle"
},
"icon": {
"type": [
"string",
"null"
]
}
}
},
"entityId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"colors": {
"type": [
"object",
"null"
],
"properties": {
"primary": {
"type": [
"string",
"null"
]
},
"secondary": {
"type": [
"string",
"null"
]
}
}
},
"fieldValues": {
"type": [
"object",
"null"
],
"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",
"null"
]
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
}
},
"ImageStyle": {
"type": "string",
"enum": [
"avatar",
"banner"
],
"default": "avatar"
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"LinkedOrganization": {
"type": "object",
"required": [
"slug"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"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",
"null"
],
"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",
"null"
],
"description": "NULL once the source replay is deleted."
},
"sourceHubSlug": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "date-time"
},
"endTime": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"Paging": {
"type": "object",
"properties": {
"next": {
"type": [
"string",
"null"
],
"description": "Pass as `cursor` to get the next page; null on the last page."
},
"hasMore": {
"type": "boolean",
"description": "Whether another page follows."
}
}
},
"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"
}
}
}
}
}
}
},
"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"
}
}
}
},
"responses": {
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# Get broadcast (org-scoped)
`GET /organizations/{orgSlug}/broadcasts/{broadcastSlug}`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "GET",
"path": "/organizations/{orgSlug}/broadcasts/{broadcastSlug}",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
},
{
"$ref": "#/components/parameters/broadcastSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"broadcastSlugParam": {
"in": "path",
"name": "broadcastSlug",
"required": true,
"schema": {
"type": "string"
}
}
},
"schemas": {
"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"
}
}
}
]
},
"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",
"null"
],
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"vodId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "ID of the attached VOD, or null when the broadcast is live."
},
"attachedVod": {
"$ref": "#/components/schemas/AttachedVodSummary"
},
"clipStartsAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"description": "The user who force-ended this broadcast, or null."
},
"endReason": {
"type": [
"string",
"null"
],
"description": "Operator-supplied note explaining the force-end, or null."
},
"archiveAttemptedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "uuid",
"description": "The auto-created default program for this broadcast. Null for VOD-mode broadcasts."
},
"activeProgramId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
]
}
}
},
"title": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"description": "The EFFECTIVE poster: a manual poster wins over a template-generated one; null falls through to the surface placeholder."
},
"generatedPosterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "uuid",
"description": "The graphics template applied to this broadcast, or null."
},
"useGeneratedPoster": {
"type": "boolean",
"description": "Per-broadcast poster-generation toggle."
},
"streamId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Null for VOD-mode broadcasts."
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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",
"null"
]
},
"defaultStreamPublicKey": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"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",
"null"
],
"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"
}
}
},
"AttachedVodSummary": {
"type": [
"object",
"null"
],
"required": [
"id",
"status",
"originalArchiveAvailable"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"status": {
"type": "string",
"enum": [
"pending",
"queued",
"processing",
"ready",
"failed"
]
},
"videoUrl": {
"type": [
"string",
"null"
],
"description": "Present when status is \"ready\""
},
"videoDuration": {
"type": [
"integer",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"description": "Original filename of the source upload."
},
"xcodeUploadId": {
"type": [
"integer",
"null"
],
"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",
"null"
],
"format": "date-time",
"description": "When the original live capture was replaced, or null when none is stashed."
}
}
},
"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."
},
"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",
"null"
]
},
"previewManifestUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"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",
"null"
]
},
"webrtc": {
"type": [
"string",
"null"
]
}
}
},
"transcode": {
"type": "boolean"
},
"authType": {
"type": [
"string",
"null"
]
},
"appData": {
"type": [
"string",
"null"
]
},
"rtmpPush": {
"type": [
"string",
"null"
],
"description": "JSON-encoded RTMP push configuration"
},
"mode": {
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}
}
}
},
"BroadcastCollectionItemResponse": {
"allOf": [
{
"$ref": "#/components/schemas/CollectionItemResponse"
},
{
"type": "object",
"properties": {
"qualifier": {
"type": [
"object",
"null"
],
"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",
"null"
],
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
}
}
},
"type": {
"type": [
"object",
"null"
],
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"system": {
"type": "boolean"
},
"imageStyle": {
"$ref": "#/components/schemas/ImageStyle"
},
"icon": {
"type": [
"string",
"null"
]
}
}
},
"entityId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"colors": {
"type": [
"object",
"null"
],
"properties": {
"primary": {
"type": [
"string",
"null"
]
},
"secondary": {
"type": [
"string",
"null"
]
}
}
},
"fieldValues": {
"type": [
"object",
"null"
],
"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",
"null"
]
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
}
},
"ImageStyle": {
"type": "string",
"enum": [
"avatar",
"banner"
],
"default": "avatar"
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"LinkedOrganization": {
"type": "object",
"required": [
"slug"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"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",
"null"
],
"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",
"null"
],
"description": "NULL once the source replay is deleted."
},
"sourceHubSlug": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "date-time"
},
"endTime": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"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"
}
}
}
},
"responses": {
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# Update broadcast (org-scoped)
`PATCH /organizations/{orgSlug}/broadcasts/{broadcastSlug}`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "PATCH",
"path": "/organizations/{orgSlug}/broadcasts/{broadcastSlug}",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
},
{
"$ref": "#/components/parameters/broadcastSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"broadcastSlugParam": {
"in": "path",
"name": "broadcastSlug",
"required": true,
"schema": {
"type": "string"
}
}
},
"schemas": {
"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",
"null"
],
"maxLength": 240
},
"posterUrl": {
"type": [
"string",
"null"
],
"format": "uri"
},
"streamId": {
"type": "string",
"format": "uuid"
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
],
"maxLength": 64
},
"graphicsTemplateId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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"
}
}
}
},
"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."
},
"BroadcastCollectionItemInput": {
"type": "object",
"required": [
"collectionItemId"
],
"properties": {
"collectionItemId": {
"type": "string",
"format": "uuid"
},
"qualifierId": {
"type": [
"string",
"null"
],
"format": "uuid"
}
}
},
"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",
"null"
],
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"vodId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "ID of the attached VOD, or null when the broadcast is live."
},
"attachedVod": {
"$ref": "#/components/schemas/AttachedVodSummary"
},
"clipStartsAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"description": "The user who force-ended this broadcast, or null."
},
"endReason": {
"type": [
"string",
"null"
],
"description": "Operator-supplied note explaining the force-end, or null."
},
"archiveAttemptedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "uuid",
"description": "The auto-created default program for this broadcast. Null for VOD-mode broadcasts."
},
"activeProgramId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
]
}
}
},
"title": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"description": "The EFFECTIVE poster: a manual poster wins over a template-generated one; null falls through to the surface placeholder."
},
"generatedPosterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "uuid",
"description": "The graphics template applied to this broadcast, or null."
},
"useGeneratedPoster": {
"type": "boolean",
"description": "Per-broadcast poster-generation toggle."
},
"streamId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Null for VOD-mode broadcasts."
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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",
"null"
]
},
"defaultStreamPublicKey": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"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",
"null"
],
"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"
}
}
},
"AttachedVodSummary": {
"type": [
"object",
"null"
],
"required": [
"id",
"status",
"originalArchiveAvailable"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"status": {
"type": "string",
"enum": [
"pending",
"queued",
"processing",
"ready",
"failed"
]
},
"videoUrl": {
"type": [
"string",
"null"
],
"description": "Present when status is \"ready\""
},
"videoDuration": {
"type": [
"integer",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"description": "Original filename of the source upload."
},
"xcodeUploadId": {
"type": [
"integer",
"null"
],
"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",
"null"
],
"format": "date-time",
"description": "When the original live capture was replaced, or null when none is stashed."
}
}
},
"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",
"null"
]
},
"previewManifestUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"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",
"null"
]
},
"webrtc": {
"type": [
"string",
"null"
]
}
}
},
"transcode": {
"type": "boolean"
},
"authType": {
"type": [
"string",
"null"
]
},
"appData": {
"type": [
"string",
"null"
]
},
"rtmpPush": {
"type": [
"string",
"null"
],
"description": "JSON-encoded RTMP push configuration"
},
"mode": {
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}
}
}
},
"BroadcastCollectionItemResponse": {
"allOf": [
{
"$ref": "#/components/schemas/CollectionItemResponse"
},
{
"type": "object",
"properties": {
"qualifier": {
"type": [
"object",
"null"
],
"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",
"null"
],
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
}
}
},
"type": {
"type": [
"object",
"null"
],
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"system": {
"type": "boolean"
},
"imageStyle": {
"$ref": "#/components/schemas/ImageStyle"
},
"icon": {
"type": [
"string",
"null"
]
}
}
},
"entityId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"colors": {
"type": [
"object",
"null"
],
"properties": {
"primary": {
"type": [
"string",
"null"
]
},
"secondary": {
"type": [
"string",
"null"
]
}
}
},
"fieldValues": {
"type": [
"object",
"null"
],
"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",
"null"
]
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
}
},
"ImageStyle": {
"type": "string",
"enum": [
"avatar",
"banner"
],
"default": "avatar"
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"LinkedOrganization": {
"type": "object",
"required": [
"slug"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"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",
"null"
],
"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",
"null"
],
"description": "NULL once the source replay is deleted."
},
"sourceHubSlug": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "date-time"
},
"endTime": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"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"
}
}
}
},
"responses": {
"BadRequestError": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ConflictError": {
"description": "Conflict",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ValidationError": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# List who changed a broadcast, newest first
`GET /organizations/{orgSlug}/broadcasts/{broadcastSlug}/changes`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "GET",
"path": "/organizations/{orgSlug}/broadcasts/{broadcastSlug}/changes",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
},
{
"$ref": "#/components/parameters/broadcastSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"broadcastSlugParam": {
"in": "path",
"name": "broadcastSlug",
"required": true,
"schema": {
"type": "string"
}
},
"cursorQuery": {
"name": "cursor",
"in": "query",
"required": false,
"description": "Opaque cursor for pagination",
"schema": {
"type": "string"
}
}
},
"schemas": {
"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",
"null"
],
"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",
"null"
],
"description": "The person's display name, falling back to the username; null when neither is known.\n"
},
"username": {
"type": [
"string",
"null"
],
"description": "The sign-in username recorded with the change."
},
"profileId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"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",
"null"
],
"description": "The OAuth client the change was made through."
},
"credentialId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "The scoped API credential, when one was used."
},
"credentialName": {
"type": [
"string",
"null"
],
"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"
}
}
},
"Paging": {
"type": "object",
"properties": {
"next": {
"type": [
"string",
"null"
],
"description": "Pass as `cursor` to get the next page; null on the last page."
},
"hasMore": {
"type": "boolean",
"description": "Whether another page follows."
}
}
},
"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"
}
}
}
},
"responses": {
"BadRequestError": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# List broadcasts in hub
`GET /organizations/{orgSlug}/hubs/{hubSlug}/broadcasts`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "GET",
"path": "/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
},
{
"$ref": "#/components/parameters/hubSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"hubSlugParam": {
"in": "path",
"name": "hubSlug",
"required": true,
"schema": {
"type": "string"
}
},
"cursorQuery": {
"name": "cursor",
"in": "query",
"required": false,
"description": "Opaque cursor for pagination",
"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"
]
}
}
},
"schemas": {
"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"
}
}
},
"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",
"null"
],
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"vodId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "ID of the attached VOD, or null when the broadcast is live."
},
"attachedVod": {
"$ref": "#/components/schemas/AttachedVodSummary"
},
"clipStartsAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"description": "The user who force-ended this broadcast, or null."
},
"endReason": {
"type": [
"string",
"null"
],
"description": "Operator-supplied note explaining the force-end, or null."
},
"archiveAttemptedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "uuid",
"description": "The auto-created default program for this broadcast. Null for VOD-mode broadcasts."
},
"activeProgramId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
]
}
}
},
"title": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"description": "The EFFECTIVE poster: a manual poster wins over a template-generated one; null falls through to the surface placeholder."
},
"generatedPosterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "uuid",
"description": "The graphics template applied to this broadcast, or null."
},
"useGeneratedPoster": {
"type": "boolean",
"description": "Per-broadcast poster-generation toggle."
},
"streamId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Null for VOD-mode broadcasts."
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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",
"null"
]
},
"defaultStreamPublicKey": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"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",
"null"
],
"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"
}
}
},
"AttachedVodSummary": {
"type": [
"object",
"null"
],
"required": [
"id",
"status",
"originalArchiveAvailable"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"status": {
"type": "string",
"enum": [
"pending",
"queued",
"processing",
"ready",
"failed"
]
},
"videoUrl": {
"type": [
"string",
"null"
],
"description": "Present when status is \"ready\""
},
"videoDuration": {
"type": [
"integer",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"description": "Original filename of the source upload."
},
"xcodeUploadId": {
"type": [
"integer",
"null"
],
"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",
"null"
],
"format": "date-time",
"description": "When the original live capture was replaced, or null when none is stashed."
}
}
},
"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."
},
"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",
"null"
]
},
"previewManifestUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"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",
"null"
]
},
"webrtc": {
"type": [
"string",
"null"
]
}
}
},
"transcode": {
"type": "boolean"
},
"authType": {
"type": [
"string",
"null"
]
},
"appData": {
"type": [
"string",
"null"
]
},
"rtmpPush": {
"type": [
"string",
"null"
],
"description": "JSON-encoded RTMP push configuration"
},
"mode": {
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}
}
}
},
"BroadcastCollectionItemResponse": {
"allOf": [
{
"$ref": "#/components/schemas/CollectionItemResponse"
},
{
"type": "object",
"properties": {
"qualifier": {
"type": [
"object",
"null"
],
"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",
"null"
],
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
}
}
},
"type": {
"type": [
"object",
"null"
],
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"system": {
"type": "boolean"
},
"imageStyle": {
"$ref": "#/components/schemas/ImageStyle"
},
"icon": {
"type": [
"string",
"null"
]
}
}
},
"entityId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"colors": {
"type": [
"object",
"null"
],
"properties": {
"primary": {
"type": [
"string",
"null"
]
},
"secondary": {
"type": [
"string",
"null"
]
}
}
},
"fieldValues": {
"type": [
"object",
"null"
],
"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",
"null"
]
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
}
},
"ImageStyle": {
"type": "string",
"enum": [
"avatar",
"banner"
],
"default": "avatar"
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"LinkedOrganization": {
"type": "object",
"required": [
"slug"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"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",
"null"
],
"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",
"null"
],
"description": "NULL once the source replay is deleted."
},
"sourceHubSlug": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "date-time"
},
"endTime": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"Paging": {
"type": "object",
"properties": {
"next": {
"type": [
"string",
"null"
],
"description": "Pass as `cursor` to get the next page; null on the last page."
},
"hasMore": {
"type": "boolean",
"description": "Whether another page follows."
}
}
},
"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"
}
}
}
}
}
}
},
"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"
}
}
}
},
"responses": {
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# Create broadcast
`POST /organizations/{orgSlug}/hubs/{hubSlug}/broadcasts`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "POST",
"path": "/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
},
{
"$ref": "#/components/parameters/hubSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"hubSlugParam": {
"in": "path",
"name": "hubSlug",
"required": true,
"schema": {
"type": "string"
}
}
},
"schemas": {
"Broadcast": {
"type": "object",
"required": [
"title"
],
"properties": {
"title": {
"type": "string",
"maxLength": 72
},
"description": {
"type": [
"string",
"null"
],
"maxLength": 240
},
"posterUrl": {
"type": [
"string",
"null"
],
"format": "uri"
},
"streamId": {
"type": "string",
"format": "uuid"
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
],
"maxLength": 64
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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"
}
}
}
},
"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."
},
"BroadcastCollectionItemInput": {
"type": "object",
"required": [
"collectionItemId"
],
"properties": {
"collectionItemId": {
"type": "string",
"format": "uuid"
},
"qualifierId": {
"type": [
"string",
"null"
],
"format": "uuid"
}
}
},
"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",
"null"
],
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"vodId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "ID of the attached VOD, or null when the broadcast is live."
},
"attachedVod": {
"$ref": "#/components/schemas/AttachedVodSummary"
},
"clipStartsAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"description": "The user who force-ended this broadcast, or null."
},
"endReason": {
"type": [
"string",
"null"
],
"description": "Operator-supplied note explaining the force-end, or null."
},
"archiveAttemptedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "uuid",
"description": "The auto-created default program for this broadcast. Null for VOD-mode broadcasts."
},
"activeProgramId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
]
}
}
},
"title": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"description": "The EFFECTIVE poster: a manual poster wins over a template-generated one; null falls through to the surface placeholder."
},
"generatedPosterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "uuid",
"description": "The graphics template applied to this broadcast, or null."
},
"useGeneratedPoster": {
"type": "boolean",
"description": "Per-broadcast poster-generation toggle."
},
"streamId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Null for VOD-mode broadcasts."
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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",
"null"
]
},
"defaultStreamPublicKey": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"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",
"null"
],
"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"
}
}
},
"AttachedVodSummary": {
"type": [
"object",
"null"
],
"required": [
"id",
"status",
"originalArchiveAvailable"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"status": {
"type": "string",
"enum": [
"pending",
"queued",
"processing",
"ready",
"failed"
]
},
"videoUrl": {
"type": [
"string",
"null"
],
"description": "Present when status is \"ready\""
},
"videoDuration": {
"type": [
"integer",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"description": "Original filename of the source upload."
},
"xcodeUploadId": {
"type": [
"integer",
"null"
],
"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",
"null"
],
"format": "date-time",
"description": "When the original live capture was replaced, or null when none is stashed."
}
}
},
"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",
"null"
]
},
"previewManifestUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"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",
"null"
]
},
"webrtc": {
"type": [
"string",
"null"
]
}
}
},
"transcode": {
"type": "boolean"
},
"authType": {
"type": [
"string",
"null"
]
},
"appData": {
"type": [
"string",
"null"
]
},
"rtmpPush": {
"type": [
"string",
"null"
],
"description": "JSON-encoded RTMP push configuration"
},
"mode": {
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}
}
}
},
"BroadcastCollectionItemResponse": {
"allOf": [
{
"$ref": "#/components/schemas/CollectionItemResponse"
},
{
"type": "object",
"properties": {
"qualifier": {
"type": [
"object",
"null"
],
"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",
"null"
],
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
}
}
},
"type": {
"type": [
"object",
"null"
],
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"system": {
"type": "boolean"
},
"imageStyle": {
"$ref": "#/components/schemas/ImageStyle"
},
"icon": {
"type": [
"string",
"null"
]
}
}
},
"entityId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"colors": {
"type": [
"object",
"null"
],
"properties": {
"primary": {
"type": [
"string",
"null"
]
},
"secondary": {
"type": [
"string",
"null"
]
}
}
},
"fieldValues": {
"type": [
"object",
"null"
],
"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",
"null"
]
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
}
},
"ImageStyle": {
"type": "string",
"enum": [
"avatar",
"banner"
],
"default": "avatar"
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"LinkedOrganization": {
"type": "object",
"required": [
"slug"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"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",
"null"
],
"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",
"null"
],
"description": "NULL once the source replay is deleted."
},
"sourceHubSlug": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "date-time"
},
"endTime": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"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"
}
}
}
},
"responses": {
"BadRequestError": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ConflictError": {
"description": "Conflict",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ValidationError": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# Bulk update broadcasts
`PATCH /organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/bulk`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "PATCH",
"path": "/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/bulk",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
},
{
"$ref": "#/components/parameters/hubSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"hubSlugParam": {
"in": "path",
"name": "hubSlug",
"required": true,
"schema": {
"type": "string"
}
}
},
"schemas": {
"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"
}
}
},
"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",
"null"
],
"maxLength": 240
},
"posterUrl": {
"type": [
"string",
"null"
],
"format": "uri"
},
"streamId": {
"type": "string",
"format": "uuid"
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
],
"maxLength": 64
},
"graphicsTemplateId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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"
}
}
}
},
"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."
},
"BroadcastCollectionItemInput": {
"type": "object",
"required": [
"collectionItemId"
],
"properties": {
"collectionItemId": {
"type": "string",
"format": "uuid"
},
"qualifierId": {
"type": [
"string",
"null"
],
"format": "uuid"
}
}
},
"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"
}
}
}
}
},
"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",
"null"
],
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"vodId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "ID of the attached VOD, or null when the broadcast is live."
},
"attachedVod": {
"$ref": "#/components/schemas/AttachedVodSummary"
},
"clipStartsAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"description": "The user who force-ended this broadcast, or null."
},
"endReason": {
"type": [
"string",
"null"
],
"description": "Operator-supplied note explaining the force-end, or null."
},
"archiveAttemptedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "uuid",
"description": "The auto-created default program for this broadcast. Null for VOD-mode broadcasts."
},
"activeProgramId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
]
}
}
},
"title": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"description": "The EFFECTIVE poster: a manual poster wins over a template-generated one; null falls through to the surface placeholder."
},
"generatedPosterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "uuid",
"description": "The graphics template applied to this broadcast, or null."
},
"useGeneratedPoster": {
"type": "boolean",
"description": "Per-broadcast poster-generation toggle."
},
"streamId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Null for VOD-mode broadcasts."
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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",
"null"
]
},
"defaultStreamPublicKey": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"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",
"null"
],
"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"
}
}
},
"AttachedVodSummary": {
"type": [
"object",
"null"
],
"required": [
"id",
"status",
"originalArchiveAvailable"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"status": {
"type": "string",
"enum": [
"pending",
"queued",
"processing",
"ready",
"failed"
]
},
"videoUrl": {
"type": [
"string",
"null"
],
"description": "Present when status is \"ready\""
},
"videoDuration": {
"type": [
"integer",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"description": "Original filename of the source upload."
},
"xcodeUploadId": {
"type": [
"integer",
"null"
],
"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",
"null"
],
"format": "date-time",
"description": "When the original live capture was replaced, or null when none is stashed."
}
}
},
"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",
"null"
]
},
"previewManifestUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"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",
"null"
]
},
"webrtc": {
"type": [
"string",
"null"
]
}
}
},
"transcode": {
"type": "boolean"
},
"authType": {
"type": [
"string",
"null"
]
},
"appData": {
"type": [
"string",
"null"
]
},
"rtmpPush": {
"type": [
"string",
"null"
],
"description": "JSON-encoded RTMP push configuration"
},
"mode": {
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}
}
}
},
"BroadcastCollectionItemResponse": {
"allOf": [
{
"$ref": "#/components/schemas/CollectionItemResponse"
},
{
"type": "object",
"properties": {
"qualifier": {
"type": [
"object",
"null"
],
"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",
"null"
],
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
}
}
},
"type": {
"type": [
"object",
"null"
],
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"system": {
"type": "boolean"
},
"imageStyle": {
"$ref": "#/components/schemas/ImageStyle"
},
"icon": {
"type": [
"string",
"null"
]
}
}
},
"entityId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"colors": {
"type": [
"object",
"null"
],
"properties": {
"primary": {
"type": [
"string",
"null"
]
},
"secondary": {
"type": [
"string",
"null"
]
}
}
},
"fieldValues": {
"type": [
"object",
"null"
],
"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",
"null"
]
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
}
},
"ImageStyle": {
"type": "string",
"enum": [
"avatar",
"banner"
],
"default": "avatar"
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"LinkedOrganization": {
"type": "object",
"required": [
"slug"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"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",
"null"
],
"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",
"null"
],
"description": "NULL once the source replay is deleted."
},
"sourceHubSlug": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "date-time"
},
"endTime": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"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"
}
}
}
},
"responses": {
"BadRequestError": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# Bulk upsert broadcasts
`POST /organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/bulk-upsert`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "POST",
"path": "/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/bulk-upsert",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
},
{
"$ref": "#/components/parameters/hubSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"hubSlugParam": {
"in": "path",
"name": "hubSlug",
"required": true,
"schema": {
"type": "string"
}
}
},
"schemas": {
"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",
"null"
],
"maxLength": 240
},
"posterUrl": {
"type": [
"string",
"null"
],
"format": "uri"
},
"streamId": {
"type": "string",
"format": "uuid"
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
],
"maxLength": 64
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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"
}
}
}
},
"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."
},
"BroadcastCollectionItemInput": {
"type": "object",
"required": [
"collectionItemId"
],
"properties": {
"collectionItemId": {
"type": "string",
"format": "uuid"
},
"qualifierId": {
"type": [
"string",
"null"
],
"format": "uuid"
}
}
},
"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"
}
}
}
}
}
},
"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",
"null"
],
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"vodId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "ID of the attached VOD, or null when the broadcast is live."
},
"attachedVod": {
"$ref": "#/components/schemas/AttachedVodSummary"
},
"clipStartsAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"description": "The user who force-ended this broadcast, or null."
},
"endReason": {
"type": [
"string",
"null"
],
"description": "Operator-supplied note explaining the force-end, or null."
},
"archiveAttemptedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "uuid",
"description": "The auto-created default program for this broadcast. Null for VOD-mode broadcasts."
},
"activeProgramId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
]
}
}
},
"title": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"description": "The EFFECTIVE poster: a manual poster wins over a template-generated one; null falls through to the surface placeholder."
},
"generatedPosterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "uuid",
"description": "The graphics template applied to this broadcast, or null."
},
"useGeneratedPoster": {
"type": "boolean",
"description": "Per-broadcast poster-generation toggle."
},
"streamId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Null for VOD-mode broadcasts."
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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",
"null"
]
},
"defaultStreamPublicKey": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"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",
"null"
],
"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"
}
}
},
"AttachedVodSummary": {
"type": [
"object",
"null"
],
"required": [
"id",
"status",
"originalArchiveAvailable"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"status": {
"type": "string",
"enum": [
"pending",
"queued",
"processing",
"ready",
"failed"
]
},
"videoUrl": {
"type": [
"string",
"null"
],
"description": "Present when status is \"ready\""
},
"videoDuration": {
"type": [
"integer",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"description": "Original filename of the source upload."
},
"xcodeUploadId": {
"type": [
"integer",
"null"
],
"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",
"null"
],
"format": "date-time",
"description": "When the original live capture was replaced, or null when none is stashed."
}
}
},
"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",
"null"
]
},
"previewManifestUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"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",
"null"
]
},
"webrtc": {
"type": [
"string",
"null"
]
}
}
},
"transcode": {
"type": "boolean"
},
"authType": {
"type": [
"string",
"null"
]
},
"appData": {
"type": [
"string",
"null"
]
},
"rtmpPush": {
"type": [
"string",
"null"
],
"description": "JSON-encoded RTMP push configuration"
},
"mode": {
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}
}
}
},
"BroadcastCollectionItemResponse": {
"allOf": [
{
"$ref": "#/components/schemas/CollectionItemResponse"
},
{
"type": "object",
"properties": {
"qualifier": {
"type": [
"object",
"null"
],
"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",
"null"
],
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
}
}
},
"type": {
"type": [
"object",
"null"
],
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"system": {
"type": "boolean"
},
"imageStyle": {
"$ref": "#/components/schemas/ImageStyle"
},
"icon": {
"type": [
"string",
"null"
]
}
}
},
"entityId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"colors": {
"type": [
"object",
"null"
],
"properties": {
"primary": {
"type": [
"string",
"null"
]
},
"secondary": {
"type": [
"string",
"null"
]
}
}
},
"fieldValues": {
"type": [
"object",
"null"
],
"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",
"null"
]
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
}
},
"ImageStyle": {
"type": "string",
"enum": [
"avatar",
"banner"
],
"default": "avatar"
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"LinkedOrganization": {
"type": "object",
"required": [
"slug"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"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",
"null"
],
"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",
"null"
],
"description": "NULL once the source replay is deleted."
},
"sourceHubSlug": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "date-time"
},
"endTime": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"InvalidField": {
"type": "object",
"required": [
"name",
"reason"
],
"properties": {
"name": {
"type": "string"
},
"reason": {
"type": "string"
},
"errorCode": {
"type": "string"
}
}
},
"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"
}
}
}
},
"responses": {
"BadRequestError": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# Get broadcast
`GET /organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/{broadcastSlug}`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "GET",
"path": "/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/{broadcastSlug}",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
},
{
"$ref": "#/components/parameters/hubSlugParam"
},
{
"$ref": "#/components/parameters/broadcastSlugParam"
}
],
"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"
}
}
}
],
"components": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"hubSlugParam": {
"in": "path",
"name": "hubSlug",
"required": true,
"schema": {
"type": "string"
}
},
"broadcastSlugParam": {
"in": "path",
"name": "broadcastSlug",
"required": true,
"schema": {
"type": "string"
}
}
},
"schemas": {
"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"
}
}
}
]
},
"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",
"null"
],
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"vodId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "ID of the attached VOD, or null when the broadcast is live."
},
"attachedVod": {
"$ref": "#/components/schemas/AttachedVodSummary"
},
"clipStartsAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"description": "The user who force-ended this broadcast, or null."
},
"endReason": {
"type": [
"string",
"null"
],
"description": "Operator-supplied note explaining the force-end, or null."
},
"archiveAttemptedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "uuid",
"description": "The auto-created default program for this broadcast. Null for VOD-mode broadcasts."
},
"activeProgramId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
]
}
}
},
"title": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"description": "The EFFECTIVE poster: a manual poster wins over a template-generated one; null falls through to the surface placeholder."
},
"generatedPosterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "uuid",
"description": "The graphics template applied to this broadcast, or null."
},
"useGeneratedPoster": {
"type": "boolean",
"description": "Per-broadcast poster-generation toggle."
},
"streamId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Null for VOD-mode broadcasts."
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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",
"null"
]
},
"defaultStreamPublicKey": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"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",
"null"
],
"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"
}
}
},
"AttachedVodSummary": {
"type": [
"object",
"null"
],
"required": [
"id",
"status",
"originalArchiveAvailable"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"status": {
"type": "string",
"enum": [
"pending",
"queued",
"processing",
"ready",
"failed"
]
},
"videoUrl": {
"type": [
"string",
"null"
],
"description": "Present when status is \"ready\""
},
"videoDuration": {
"type": [
"integer",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"description": "Original filename of the source upload."
},
"xcodeUploadId": {
"type": [
"integer",
"null"
],
"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",
"null"
],
"format": "date-time",
"description": "When the original live capture was replaced, or null when none is stashed."
}
}
},
"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."
},
"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",
"null"
]
},
"previewManifestUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"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",
"null"
]
},
"webrtc": {
"type": [
"string",
"null"
]
}
}
},
"transcode": {
"type": "boolean"
},
"authType": {
"type": [
"string",
"null"
]
},
"appData": {
"type": [
"string",
"null"
]
},
"rtmpPush": {
"type": [
"string",
"null"
],
"description": "JSON-encoded RTMP push configuration"
},
"mode": {
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}
}
}
},
"BroadcastCollectionItemResponse": {
"allOf": [
{
"$ref": "#/components/schemas/CollectionItemResponse"
},
{
"type": "object",
"properties": {
"qualifier": {
"type": [
"object",
"null"
],
"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",
"null"
],
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
}
}
},
"type": {
"type": [
"object",
"null"
],
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"system": {
"type": "boolean"
},
"imageStyle": {
"$ref": "#/components/schemas/ImageStyle"
},
"icon": {
"type": [
"string",
"null"
]
}
}
},
"entityId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"colors": {
"type": [
"object",
"null"
],
"properties": {
"primary": {
"type": [
"string",
"null"
]
},
"secondary": {
"type": [
"string",
"null"
]
}
}
},
"fieldValues": {
"type": [
"object",
"null"
],
"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",
"null"
]
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
}
},
"ImageStyle": {
"type": "string",
"enum": [
"avatar",
"banner"
],
"default": "avatar"
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"LinkedOrganization": {
"type": "object",
"required": [
"slug"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"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",
"null"
],
"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",
"null"
],
"description": "NULL once the source replay is deleted."
},
"sourceHubSlug": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "date-time"
},
"endTime": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"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"
}
}
}
},
"responses": {
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```
# Update broadcast
`PATCH /organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/{broadcastSlug}`
- Base URL: `https:///eventdoor/api/v1` (copy yours from Settings > Integrations in the admin).
- Authentication: `Authorization: Bearer edk_...` (https://docs.eventdoor.com/rest-api/authentication.md).
- Errors: https://docs.eventdoor.com/rest-api/errors.md. Pagination: https://docs.eventdoor.com/rest-api/pagination.md.
- Full spec: https://docs.eventdoor.com/rest-api/openapi.json. Every `$ref` below resolves against `components` in the same JSON.
```json
{
"operations": [
{
"method": "PATCH",
"path": "/organizations/{orgSlug}/hubs/{hubSlug}/broadcasts/{broadcastSlug}",
"pathParameters": [
{
"$ref": "#/components/parameters/orgSlugParam"
},
{
"$ref": "#/components/parameters/hubSlugParam"
},
{
"$ref": "#/components/parameters/broadcastSlugParam"
}
],
"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": {
"securitySchemes": {
"ApiCredential": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "edk__",
"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)."
}
},
"parameters": {
"orgSlugParam": {
"in": "path",
"name": "orgSlug",
"required": true,
"schema": {
"type": "string"
}
},
"hubSlugParam": {
"in": "path",
"name": "hubSlug",
"required": true,
"schema": {
"type": "string"
}
},
"broadcastSlugParam": {
"in": "path",
"name": "broadcastSlug",
"required": true,
"schema": {
"type": "string"
}
}
},
"schemas": {
"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",
"null"
],
"maxLength": 240
},
"posterUrl": {
"type": [
"string",
"null"
],
"format": "uri"
},
"streamId": {
"type": "string",
"format": "uuid"
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
],
"maxLength": 64
},
"graphicsTemplateId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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"
}
}
}
},
"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."
},
"BroadcastCollectionItemInput": {
"type": "object",
"required": [
"collectionItemId"
],
"properties": {
"collectionItemId": {
"type": "string",
"format": "uuid"
},
"qualifierId": {
"type": [
"string",
"null"
],
"format": "uuid"
}
}
},
"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",
"null"
],
"format": "uuid"
},
"organizationId": {
"type": "string",
"format": "uuid"
},
"vodId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "ID of the attached VOD, or null when the broadcast is live."
},
"attachedVod": {
"$ref": "#/components/schemas/AttachedVodSummary"
},
"clipStartsAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "date-time",
"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",
"null"
],
"description": "The user who force-ended this broadcast, or null."
},
"endReason": {
"type": [
"string",
"null"
],
"description": "Operator-supplied note explaining the force-end, or null."
},
"archiveAttemptedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"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",
"null"
],
"format": "uuid",
"description": "The auto-created default program for this broadcast. Null for VOD-mode broadcasts."
},
"activeProgramId": {
"type": [
"string",
"null"
],
"format": "uuid",
"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",
"null"
]
}
}
},
"title": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"description": "The EFFECTIVE poster: a manual poster wins over a template-generated one; null falls through to the surface placeholder."
},
"generatedPosterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "uuid",
"description": "The graphics template applied to this broadcast, or null."
},
"useGeneratedPoster": {
"type": "boolean",
"description": "Per-broadcast poster-generation toggle."
},
"streamId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Null for VOD-mode broadcasts."
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"timezone": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"minimum": 0,
"maximum": 240,
"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",
"null"
],
"minimum": 0,
"maximum": 720,
"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",
"null"
]
},
"defaultStreamPublicKey": {
"type": [
"string",
"null"
]
},
"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",
"null"
],
"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",
"null"
],
"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"
}
}
},
"AttachedVodSummary": {
"type": [
"object",
"null"
],
"required": [
"id",
"status",
"originalArchiveAvailable"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"status": {
"type": "string",
"enum": [
"pending",
"queued",
"processing",
"ready",
"failed"
]
},
"videoUrl": {
"type": [
"string",
"null"
],
"description": "Present when status is \"ready\""
},
"videoDuration": {
"type": [
"integer",
"null"
]
},
"posterUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"description": "Original filename of the source upload."
},
"xcodeUploadId": {
"type": [
"integer",
"null"
],
"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",
"null"
],
"format": "date-time",
"description": "When the original live capture was replaced, or null when none is stashed."
}
}
},
"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",
"null"
]
},
"previewManifestUrl": {
"type": [
"string",
"null"
],
"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",
"null"
],
"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",
"null"
]
},
"webrtc": {
"type": [
"string",
"null"
]
}
}
},
"transcode": {
"type": "boolean"
},
"authType": {
"type": [
"string",
"null"
]
},
"appData": {
"type": [
"string",
"null"
]
},
"rtmpPush": {
"type": [
"string",
"null"
],
"description": "JSON-encoded RTMP push configuration"
},
"mode": {
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}
}
}
},
"BroadcastCollectionItemResponse": {
"allOf": [
{
"$ref": "#/components/schemas/CollectionItemResponse"
},
{
"type": "object",
"properties": {
"qualifier": {
"type": [
"object",
"null"
],
"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",
"null"
],
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
}
}
},
"type": {
"type": [
"object",
"null"
],
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"system": {
"type": "boolean"
},
"imageStyle": {
"$ref": "#/components/schemas/ImageStyle"
},
"icon": {
"type": [
"string",
"null"
]
}
}
},
"entityId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"avatarUrl": {
"type": [
"string",
"null"
]
},
"bannerUrl": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"colors": {
"type": [
"object",
"null"
],
"properties": {
"primary": {
"type": [
"string",
"null"
]
},
"secondary": {
"type": [
"string",
"null"
]
}
}
},
"fieldValues": {
"type": [
"object",
"null"
],
"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",
"null"
]
},
"gate": {
"type": [
"object",
"null"
],
"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"
}
}
}
},
"ImageStyle": {
"type": "string",
"enum": [
"avatar",
"banner"
],
"default": "avatar"
},
"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",
"null"
],
"description": "Viewer-facing product label (falls back to the internal name)."
},
"buttonLabel": {
"type": [
"string",
"null"
],
"description": "The gate's buy-button label; null falls back to the product label."
},
"showPrice": {
"type": "boolean"
},
"unitAmount": {
"type": [
"integer",
"null"
],
"description": "Price in minor units (e.g. cents)."
},
"currency": {
"type": [
"string",
"null"
],
"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."
}
}
},
"LinkedOrganization": {
"type": "object",
"required": [
"slug"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},
"avatarUrl": {
"type": [
"string",
"null"
]
}
}
},
"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",
"null"
],
"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",
"null"
],
"description": "NULL once the source replay is deleted."
},
"sourceHubSlug": {
"type": [
"string",
"null"
],
"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",
"null"
],
"format": "date-time"
},
"endTime": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"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"
}
}
}
},
"responses": {
"BadRequestError": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"UnauthorizedError": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ForbiddenError": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFoundError": {
"description": "Not Found Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ConflictError": {
"description": "Conflict",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"ValidationError": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"TooManyRequestsError": {
"description": "Too many requests - retry later",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"InternalServerError": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
```