Getting started
What is Rundown Studio?Create an accountRundown
Rundown basicsColumnsTemplatesSettingsTrashCell historyMentionsText variablesRunning a showImport CSV rundownEvent
Event basicsSharing eventsAPI
Getting startedAPI reference ↗Build a rundown from CSVWorking with cell contentLive updates over SSEAdvanced usageError referenceMigrating from v0API v0 (deprecated)Integrations
Companion ModuleQLabSharing and outputs
Read-only rundownEditable rundownOutputPrompterPDF exportCSV exportAccount
Your teamSubscription and invoicesUpdates
ChangelogError reference
Every v1 error code — what triggers it, what the response carries, and how to recover.
Every error the API returns is an RFC 9457 application/problem+json document. The type URL points at this page, anchored to the specific code — if an error response brought you here, you should be looking at the exact entry for the code you received.
A full error document looks like this:
{
"type": "https://rundownstudio.app/docs/api-v1/errors#cues.not_found",
"title": "Not Found",
"status": 404,
"code": "cues.not_found",
"detail": "Cue `myA2WwGus2YAqE4yY6qq` does not exist in rundown `YOmm4UG2SGSomuOci44c` or has been deleted.",
"instance": "/rundowns/YOmm4UG2SGSomuOci44c/cues/myA2WwGus2YAqE4yY6qq",
"request_id": "req_8f4c2b6d9e1a7c3b5d8e2f01",
"server_time": 1747000000000,
"spec_url": "https://api-v1.rundownstudio.app/openapi.yml",
"documentation_url": "https://rundownstudio.app/docs/api-v1/errors#cues.not_found",
"details": { "rundown_id": "YOmm4UG2SGSomuOci44c", "cue_id": "myA2WwGus2YAqE4yY6qq" }
}
codeis the stable, dot-namespaced identifier to branch on —detailstrings can be reworded, codes never change meaning.detailsays what went wrong and the corrective action.details(optional) carries structured data about the failure — validation issues, limits, conflicting IDs.request_idcorrelates with our server logs — quote it when contacting support.
The per-code entries below show the fields that vary: status, trigger, an example detail, and the details payload where one exists.
All codes
Authentication (401)
All auth.* codes return 401 Unauthorized.
auth.token_missing
- When: no API token was presented — no
Authorizationheader and no?token=query parameter. - Example detail: “No API token presented. Send
Authorization: Bearer <token>(preferred) or?token=<token>on control-plane GETs.” - Recover: add the
Authorization: Bearer <token>header. Mint a token in the dashboard’s API section if you don’t have one — see Getting started.
auth.malformed_authorization
- When: an
Authorizationheader is present but isn’t inBearer <token>form (wrong scheme, missing value). - Example detail: “Authorization header is present but not in
Bearer <token>form. Remove it or fix the format; do not rely on the ?token= query as a fallback.” - Recover: the header must be exactly
Authorization: Bearer <token>— check for a missingBearerprefix or a stray scheme likeBasic.
auth.invalid_token
- When: the token doesn’t verify — bad signature, malformed claims, or it wasn’t issued for this environment.
- Example detail: “API token signature is invalid. Check the token value and that it was issued for this environment.”
- Recover: re-copy the token from the dashboard. Note that v0 tokens do not work on the v1 API — they are signed with a different key. Mint a fresh v1 token.
auth.token_expired
- When: the token’s expiry timestamp is in the past.
- Example detail: “API token has expired. Mint a new token in the dashboard.”
- Recover: mint a new token in the dashboard.
GET /authon a valid token tells you itsexpires_atso you can rotate ahead of time.
auth.token_revoked
- When: the token has been revoked, or all tokens for its scope were rotated after it was issued.
- Example detail: “API token has been revoked. Mint a new one in the dashboard.”
- Recover: mint a new token. If you didn’t revoke it yourself, someone on your team did — likely deliberately.
auth.revocation_check_unavailable
- When: the backend that verifies token revocation is unreachable. Authentication fails closed — the API returns
401rather than accepting a token it can’t check. - Example detail: “Token revocation backend is unreachable. Retry shortly; if the issue persists, status.rundownstudio.app will reflect it.”
- Recover: this is a server-side condition, not a problem with your token. Retry with backoff.
auth.query_token_not_allowed
- When: a
?token=query parameter was used on a data-plane endpoint. Query tokens are accepted only on the control surface (/rundowns/:id/status,/countdown,/output-message, and theaction:<verb>POSTs). - Example detail: “
?token=query auth is accepted only on the control endpoints. UseAuthorization: Bearer <token>for this route.” - Recover: move the token into the
Authorization: Bearer <token>header.
Generic codes
bad_request
- Status:
400 Bad Request. - When: a query parameter or request body failed validation and no resource-specific code applies.
- Example detail: “
limitmust be an integer between 1 and 100.” - Details: usually
{ "issues": [{ "path": [...], "message": "...", "code": "..." }] }— one entry per offending field. - Recover: fix the field named in
detail/details.issues[].pathand retry.
not_found
- Status:
404 Not Found. - When: the requested route or resource doesn’t exist, was deleted, or is invisible to this token. Specific resources use their own code (
rundowns.not_found,cues.not_found, …); this is the catch-all, including unknown routes. - Recover: check the URL path against the API reference. If the path is right, the ID is wrong or the token’s scope doesn’t cover the resource —
GET /auth/probe?rundown_id=…tells you whether this token can see a given rundown.
forbidden
- Status:
403 Forbidden. - When: the token is valid but its role doesn’t satisfy the endpoint — e.g. a
VIEWtoken attempting a write, or anEDITtoken attempting a delete (deletes needOWNER). - Example detail: “The token’s role is insufficient for this action. Mint a higher-role token (EDIT / SHOWCALL / OWNER) in the dashboard.”
- Recover: mint a token with the role the endpoint needs. Roles build on each other:
VIEW→SHOWCALL→EDIT→OWNER.
internal / internal.unhandled
- Status:
500 Internal Server Error. - When: something unexpected broke on our side.
- Example detail: “Unexpected internal error. Reference the request_id when contacting support.”
- Recover: retry with backoff. If it persists, contact support and quote the
request_id— it points straight at our logs.
Limits and caps
rate_limit.exceeded
- Status:
429 Too Many Requests. - When: your team used up its per-minute request budget — 20/min on the free plan, 120/min on paid plans. All tokens of one team share the budget.
- Example detail: “Rate limit reached for your team (20/min on the free plan). Upgrade for 120/min. See RateLimit-* response headers for the limit, current usage, and reset time.”
- Details:
{ "tier": "free", "limit": 20, "window_seconds": 60, "retry_after_seconds": 12 } - Recover: wait for the number of seconds in the
Retry-Afterheader, then resume. Pace your client with theRateLimit-Remaining/RateLimit-Resetheaders that ride on every response. If you’re polling show status, compute the countdown locally between polls.
payload.too_large
- Status:
413 Payload Too Large. - When: the request body exceeds 1 MB — or a
?include=cellsread would return more than 1,000 cells. - Details:
{ "limit": 1048576, "received": 1840256, "unit": "bytes" } - Recover: split the write into smaller
:bulkbatches. For oversized reads, use the list endpoints (GET /rundowns/:id/cellswith pagination) instead of?include=cells.
bulk.too_many_items
- Status:
422 Unprocessable Entity. - When: a bulk array carries more than 500 items.
- Details:
{ "limit": 500, "received": 501, "unit": "items", "resource": "cues" } - Recover: split into batches of ≤500 and send them sequentially.
plan.limit_exceeded
- Status:
422 Unprocessable Entity. - When: the operation would exceed a quota of your team’s plan (e.g. the free plan’s rundown count).
- Example detail: “rundowns limit exceeded: 11 would exceed your plan cap of 10. Upgrade your plan to lift this limit.”
- Details:
{ "limit": 10, "received": 11, "unit": "rundowns", "resource": "rundowns" }— branch onresourceto see which quota tripped. - Recover: delete unused resources or upgrade the plan. Unlike the structural caps above (which are the same for everyone), this one is plan-dependent.
Idempotency
idempotency.invalid_key
- Status:
400 Bad Request. - When: the
Idempotency-Keyheader is present but malformed — longer than 256 characters. - Details:
{ "key_length": 300, "max": 256 } - Recover: use a short opaque value — a UUID is perfect.
idempotency.in_flight
- Status:
409 Conflict. - When: another request with the same
Idempotency-Keyis still being processed. - Recover: wait briefly and retry with the same key — you’ll get the original request’s cached response. If it persists past ~30 seconds, the original attempt may have died; retry once more with the same key.
idempotency.cache_corrupt
- Status:
500 Internal Server Error. - When: the response cached under this key couldn’t be decoded (rare).
- Recover: the original write may or may not have succeeded — read the resource to verify state, then retry with a fresh key if needed.
Pagination
cursor.invalid
- Status:
400 Bad Request. - When: the
?cursor=value couldn’t be decoded — it’s not a cursor this API issued, or it got truncated. - Example detail: “Pagination cursor could not be decoded. Pass the
next_cursorvalue from the previous response verbatim.” - Recover: cursors are opaque — pass
next_cursorback exactly as received, or omit?cursorto start from the first page.
Rundowns
rundowns.not_found
- Status:
404 Not Found. - When: the rundown ID doesn’t exist, was deleted, or isn’t visible to this token’s scope.
- Recover:
GET /rundownslists what the token can see;GET /auth/probe?rundown_id=…checks a specific ID.
rundowns.invalid_payload
- Status:
400 Bad Request. - When: a rundown create/patch body failed validation — missing title, cells on a
heading/groupcue, duplicate column names, unknown timezone, and so on. - Example detail: “Invalid payload at
columns[1].name: duplicate column name ‘AUDIO’ — names must be unique within columns[]” - Details:
{ "issues": [{ "path": ["columns", 1, "name"], "message": "...", "code": "..." }] } - Recover: fix the field at
details.issues[].pathand retry. OnPOST /rundownsnothing is created when validation fails — the create is all-or-nothing. See Build a rundown from CSV for the common pitfalls.
rundowns.empty_patch
- Status:
422 Unprocessable Entity. - When: a PATCH body has no fields.
- Example detail: “PATCH body has no fields. Provide at least one of: title, timezone, start_time, status, settings.”
- Recover: include at least one field you want to change.
Cues
cues.not_found
- Status:
404 Not Found. - When: the cue ID doesn’t exist in the addressed rundown, or was deleted.
- Details:
{ "rundown_id": "...", "cue_id": "..." } - Recover:
GET /rundowns/:id/cueslists the rundown’s cues with their IDs.
cues.invalid_payload
- Status:
400 Bad Request. - When: a cue create/patch body failed validation (bad
type, over-long title, negative duration, malformed position descriptor, …). - Details:
{ "issues": [{ "path": [...], "message": "...", "code": "..." }] } - Recover: fix the field at
details.issues[].pathand retry.
cues.empty_patch
- Status:
422 Unprocessable Entity. - When: a cue PATCH body has no fields.
- Recover: include at least one field:
title,subtitle,duration_ms,background_color,start_time,type, orposition.
cues.too_many
- Status:
422 Unprocessable Entity. - When: the operation would push the rundown past the 1,000-cue cap (group children count).
- Details:
{ "limit": 1000, "received": 1001, "unit": "cues" } - Recover: the cap is structural and the same for everyone. Split the show across multiple rundowns.
cues.invalid_position
- Status:
422 Unprocessable Entity. - When: a position descriptor (
{ before_cue_id },{ after_cue_id },{ start },{ end }) is structurally valid but semantically impossible — e.g. it would nest a group inside a group. - Example detail: “Invalid position descriptor: cannot create a group inside another group. Place the group at root level, or use
:reorderfor hierarchical edits.” - Recover: pick a target that respects the hierarchy rules: groups live at root level only; group children are cues and headings.
cues.position_target_not_found
- Status:
422 Unprocessable Entity. - When: the cue referenced by
before_cue_id/after_cue_iddoesn’t exist in this rundown. - Details:
{ "cue_id": "..." } - Recover: confirm the target ID against
GET /rundowns/:id/cues— it may have been deleted since you read it.
cues.invalid_cue_type
- Status:
422 Unprocessable Entity. - When: the operation requires a specific cue type the target isn’t — most commonly
action:jumpaimed at aheadingorgroup. - Example detail: “Cannot jump to a group. Jump targets must be regular cues (heading/group are layout markers).”
- Recover: target a cue of
type: "cue".
cues.invalid_type_conversion
- Status:
422 Unprocessable Entity. - When: a
cue ↔ heading ↔ groupconversion isn’t allowed in the current shape — e.g. converting a cue that sits inside a group into a group (groups don’t nest). - Details:
{ "cue_id": "...", "from_type": "cue", "to_type": "group" } - Recover: move the cue out of its parent group first, then convert.
cues.group_not_empty
- Status:
409 Conflict. - When: deleting or converting a group that still has children.
- Details:
{ "cue_id": "...", "children_ids": ["...", "..."] } - Recover: relocate the children with
POST /rundowns/:id/cues:reorderor delete them individually, then retry.
cues.invalid_reorder
- Status:
422 Unprocessable Entity. - When: a
cues:reorderbody doesn’t describe a valid permutation — it adds or drops cues, duplicates an ID, parents cues under a non-group, or nests groups. - Details: the failing aspect, e.g.
{ "duplicates": [...] },{ "unknown_ids": [...] },{ "missing_ids": [...] },{ "invalid_parents": [...] }, or{ "nested_groups": [...] }. - Recover: the reorder body must contain exactly the rundown’s current cue IDs, each once, with groups only at root level.
cues.prevent_edits
- Status:
423 Locked. - When: the target cue has
prevent_edits: true— its fields reject every PATCH. (Cell writes to a locked cue fail withcells.prevent_editsinstead.) - Example detail: “Cue “Opening segment” has
prevent_edits: true— its contents are locked.” - Recover: the lock is a deliberate human-only toggle and cannot be lifted via the API. In the rundown screen: cue → Settings → uncheck Prevent Edits.
Columns
columns.not_found
- Status:
404 Not Found. - When: the column ID doesn’t exist in this rundown — or it refers to a private column, which the public API does not expose.
- Details:
{ "rundown_id": "...", "column_id": "..." } - Recover:
GET /rundowns/:id/columnslists the columns visible to the API.
columns.invalid_payload
- Status:
400 Bad Request. - When: a column create/patch body failed validation (empty name, over-long name, …).
- Details:
{ "issues": [...] } - Recover: fix the field at
details.issues[].pathand retry.
columns.empty_patch
- Status:
422 Unprocessable Entity. - When: a column PATCH body has no fields.
- Recover: provide
name.
columns.invalid_position
- Status:
422 Unprocessable Entity. - When:
before_column_id/after_column_idreferences a column that doesn’t exist or is private. - Details:
{ "after_column_id": "..." }or{ "before_column_id": "..." } - Recover: confirm the target against
GET /rundowns/:id/columns.
columns.invalid_reorder
- Status:
422 Unprocessable Entity. - When: a
columns:reorderbody’s ID set doesn’t exactly match the rundown’s public columns (duplicates, unknown IDs, missing IDs). - Details: the failing aspect:
{ "duplicates": [...] },{ "unknown_ids": [...] }, or{ "missing_ids": [...] }. - Recover: send exactly the current public column IDs, each once, in the new order. Private columns are invisible to the API and keep their relative placement.
columns.too_many
- Status:
422 Unprocessable Entity. - When: the rundown is at the 50-column cap (public and private columns both count).
- Details:
{ "limit": 50, "received": 51, "unit": "columns" } - Recover: delete a column before adding another. Note the cap counts private columns you can’t see via the API.
Cells
Cells have no ID of their own — they’re addressed by the (cue_id, column_id) coordinate, and an empty coordinate reads back as an empty cell. That’s why there is no cells.not_found: reads of unpopulated coordinates succeed with empty content.
cells.invalid_payload
- Status:
400 Bad Request. - When: a cell write body failed validation — missing coordinate fields, neither or both of
content/content_html, content over the 100,000-character cap. - Details:
{ "issues": [...] } - Recover: every write needs the coordinate plus exactly one of
content(plain text) orcontent_html(rich). See Working with cell content.
cells.invalid_query
- Status:
400 Bad Request. - When:
GET /rundowns/:id/cellsquery parameters don’t form a valid coordinate or scope. - Details:
{ "issues": [...] } - Recover: fix the named query parameter.
cells.invalid_cue_type
- Status:
422 Unprocessable Entity. - When: a cell write addressed a
headingorgroupcue — cells only exist on regular cues. - Example detail: “Cells cannot be written to a
headingcue. Cells only attach tocue-typed rows;headingandgroupare structural.” - Details:
{ "cue_id": "...", "cue_type": "heading" } - Recover: only write cells against cues of
type: "cue".
cells.duplicate_coordinate
- Status:
422 Unprocessable Entity. - When: a
cells:bulkbody contains two entries with the same(cue_id, column_id)coordinate. - Details:
{ "duplicates": [{ "cue_id": "...", "column_id": "...", "indexes": [0, 3] }] } - Recover: deduplicate the batch — duplicate writes within one batch would be non-deterministic, so the whole batch is rejected.
cells.unsupported_column_type
- Status:
422 Unprocessable Entity. - When: the write targets a non-text column. The API currently writes
textcolumns only; dropdown / image / attachment columns are read-only on the wire. - Details:
{ "column_id": "...", "column_type": "select", "supported": ["text"] } - Recover: richer column types are planned but not writable yet — edit those in the dashboard.
cells.prevent_edits
- Status:
409 Conflict. - When: the cell’s cue has
prevent_edits: true, which locks its cells too. - Details:
{ "cue_id": "..." } - Recover: like
cues.prevent_edits, the lock is a human-only toggle — released in the rundown screen, not via the API.
Mentions
mentions.not_found
- Status:
404 Not Found. - When: the mention ID doesn’t exist in this rundown.
- Details:
{ "rundown_id": "...", "mention_id": "..." } - Recover:
GET /rundowns/:id/mentionslists all mentions with their IDs.
mentions.invalid_payload
- Status:
400 Bad Request. - When: a mention create/patch/bulk body failed validation.
- Details:
{ "issues": [...] } - Recover: fix the field at
details.issues[].pathand retry.
mentions.empty_patch
- Status:
422 Unprocessable Entity. - When: a mention PATCH body has no fields.
- Recover: provide at least one of
name,color, ordescription.
mentions.duplicate_id
- Status:
422 Unprocessable Entity. - When: a
mentions:bulkbody reuses the same mention ID twice. - Details:
{ "duplicates": [{ "id": "...", "indexes": [0, 2] }] } - Recover: deduplicate the batch and retry.
mentions.name_conflict
- Status:
422 Unprocessable Entity. - When: creating a mention with a name that already exists — mention names are unique per rundown.
- Details:
{ "rundown_id": "...", "name": "..." } - Recover: update the existing mention instead, or pick a different name.
mentions.too_many
- Status:
422 Unprocessable Entity. - When: the rundown is at the 100-mention cap.
- Details:
{ "rundown_id": "...", "max": 100 } - Recover: delete a mention before adding another.
Text variables
text_variables.not_found
- Status:
404 Not Found. - When: no text variable with this key exists in the rundown.
- Details:
{ "key": "...", "rundown_id": "..." } - Recover:
GET /rundowns/:id/text-variableslists all keys.
text_variables.invalid_payload
- Status:
400 Bad Request. - When: the key or value failed validation (bad characters in the key, value too long).
- Details:
{ "key": "...", "message": "..." }or{ "issues": [...] } - Recover: fix the named field and retry.
text_variables.duplicate_key
- Status:
422 Unprocessable Entity. - When: a
text-variables:bulkbody reuses the same key twice. - Details:
{ "duplicates": ["KEY1", "KEY2"] } - Recover: deduplicate the batch and retry.
text_variables.key_exists
- Status:
422 Unprocessable Entity. - When: creating a variable with a key that already exists.
- Example detail: “Text variable with key “ANCHOR_NAME” already exists. Use PATCH /text-variables/ANCHOR_NAME to update its value.”
- Recover: update the existing key instead of creating it.
text_variables.too_many
- Status:
422 Unprocessable Entity. - When: the operation would exceed the 100-variable cap per rundown.
- Details:
{ "attempted": 101 } - Recover: delete unused variables, or with
text-variables:bulkusemode: "replace"to swap the whole set.
Show control
The runner.* codes come from the show state machine. All return 409 Conflict — the request was well-formed but the show isn’t in a state that allows it. GET /rundowns/:id/status tells you the current state.
runner.not_running
- When: the action requires a running show (
action:pause,action:adjust-duration) but the rundown is stopped — pre-show or ended. - Recover: send
action:startfirst, or checkstatusbefore acting.
runner.ended_cannot_start
- When:
action:startwas sent to a show that has already ended. - Recover: an ended show can’t be restarted via the API — reset it from the dashboard.
runner.no_next_cue
- When:
action:nextfrom the last playable cue. - Recover: nothing to advance to — the show is on its final cue.
runner.no_previous_cue
- When:
action:previousfrom the first playable cue. - Recover: nothing to step back to.
messages.empty_patch
- Status:
422 Unprocessable Entity. - When: a
PATCH /rundowns/:id/output-messagebody has no fields. - Recover: provide at least one of
text,visible,color,bold,underline. Showing and hiding the message isvisible: true/visible: false— there are no separate show/hide endpoints in v1.