Getting started

What is Rundown Studio?Create an account

Rundown

Rundown basicsColumnsTemplatesSettingsTrashCell historyMentionsText variablesRunning a showImport CSV rundown

Event

Event basicsSharing events

API

Getting startedAPI reference ↗Build a rundown from CSVWorking with cell contentLive updates over SSEAdvanced usageError referenceMigrating from v0API v0 (deprecated)

Integrations

Companion ModuleQLab

Sharing and outputs

Read-only rundownEditable rundownOutputPrompterPDF exportCSV export

Account

Your teamSubscription and invoices

Updates

Changelog
Docs API

Error reference

Error 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" }
}
  • code is the stable, dot-namespaced identifier to branch on — detail strings can be reworded, codes never change meaning.
  • detail says what went wrong and the corrective action.
  • details (optional) carries structured data about the failure — validation issues, limits, conflicting IDs.
  • request_id correlates 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

FamilyCodes
Authenticationauth.token_missing · auth.malformed_authorization · auth.invalid_token · auth.token_expired · auth.token_revoked · auth.revocation_check_unavailable · auth.query_token_not_allowed
Genericbad_request · not_found · forbidden · internal
Limits & capsrate_limit.exceeded · payload.too_large · bulk.too_many_items · plan.limit_exceeded
Idempotencyidempotency.invalid_key · idempotency.in_flight · idempotency.cache_corrupt
Paginationcursor.invalid
Rundownsrundowns.not_found · rundowns.invalid_payload · rundowns.empty_patch
Cuescues.not_found · cues.invalid_payload · cues.empty_patch · cues.too_many · cues.invalid_position · cues.position_target_not_found · cues.invalid_cue_type · cues.invalid_type_conversion · cues.group_not_empty · cues.invalid_reorder · cues.prevent_edits
Columnscolumns.not_found · columns.invalid_payload · columns.empty_patch · columns.invalid_position · columns.invalid_reorder · columns.too_many
Cellscells.invalid_payload · cells.invalid_query · cells.invalid_cue_type · cells.duplicate_coordinate · cells.unsupported_column_type · cells.prevent_edits
Mentionsmentions.not_found · mentions.invalid_payload · mentions.empty_patch · mentions.duplicate_id · mentions.name_conflict · mentions.too_many
Text variablestext_variables.not_found · text_variables.invalid_payload · text_variables.duplicate_key · text_variables.key_exists · text_variables.too_many
Show controlrunner.not_running · runner.ended_cannot_start · runner.no_next_cue · runner.no_previous_cue · messages.empty_patch

Authentication (401)

All auth.* codes return 401 Unauthorized.

auth.token_missing

  • When: no API token was presented — no Authorization header 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 Authorization header is present but isn’t in Bearer <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 missing Bearer prefix or a stray scheme like Basic.

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 /auth on a valid token tells you its expires_at so 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 401 rather 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 the action:<verb> POSTs).
  • Example detail:?token= query auth is accepted only on the control endpoints. Use Authorization: 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:limit must 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[].path and 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 VIEW token attempting a write, or an EDIT token attempting a delete (deletes need OWNER).
  • 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: VIEWSHOWCALLEDITOWNER.

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-After header, then resume. Pace your client with the RateLimit-Remaining / RateLimit-Reset headers 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=cells read would return more than 1,000 cells.
  • Details: { "limit": 1048576, "received": 1840256, "unit": "bytes" }
  • Recover: split the write into smaller :bulk batches. For oversized reads, use the list endpoints (GET /rundowns/:id/cells with 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 on resource to 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-Key header 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-Key is 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_cursor value from the previous response verbatim.”
  • Recover: cursors are opaque — pass next_cursor back exactly as received, or omit ?cursor to 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 /rundowns lists 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/group cue, 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[].path and retry. On POST /rundowns nothing 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/cues lists 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[].path and 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, or position.

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 :reorder for 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_id doesn’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:jump aimed at a heading or group.
  • 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 ↔ group conversion 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:reorder or delete them individually, then retry.

cues.invalid_reorder

  • Status: 422 Unprocessable Entity.
  • When: a cues:reorder body 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 with cells.prevent_edits instead.)
  • 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/columns lists 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[].path and 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_id references 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:reorder body’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) or content_html (rich). See Working with cell content.

cells.invalid_query

  • Status: 400 Bad Request.
  • When: GET /rundowns/:id/cells query 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 heading or group cue — cells only exist on regular cues.
  • Example detail: “Cells cannot be written to a heading cue. Cells only attach to cue-typed rows; heading and group are 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:bulk body 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 text columns 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/mentions lists 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[].path and retry.

mentions.empty_patch

  • Status: 422 Unprocessable Entity.
  • When: a mention PATCH body has no fields.
  • Recover: provide at least one of name, color, or description.

mentions.duplicate_id

  • Status: 422 Unprocessable Entity.
  • When: a mentions:bulk body 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-variables lists 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:bulk body 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:bulk use mode: "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:start first, or check status before acting.

runner.ended_cannot_start

  • When: action:start was 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:next from the last playable cue.
  • Recover: nothing to advance to — the show is on its final cue.

runner.no_previous_cue

  • When: action:previous from the first playable cue.
  • Recover: nothing to step back to.

messages.empty_patch

  • Status: 422 Unprocessable Entity.
  • When: a PATCH /rundowns/:id/output-message body has no fields.
  • Recover: provide at least one of text, visible, color, bold, underline. Showing and hiding the message is visible: true / visible: false — there are no separate show/hide endpoints in v1.