API overview

Docs API reference

API overview

Authentication with API keys, scopes, refs and idempotent upserts, errors, paging and times.

The Wunderframe API lets your own systems do what an editor does in the app: import presenters, plan sessions, keep the words on your screens up to date and upload media. It is a JSON REST API with one kind of key, one error shape, and an OpenAPI 3.1 document you can load into Postman, Insomnia or a client generator.

Event sync For an event system that owns the program — Maravilla Studio’s Event Suite, or your own: send the whole event (rooms, speakers, sessions, tracks, translations) and Wunderframe makes or updates the agenda records, presenters, rooms, live sessions and shows, idempotently by your keys. Set the workspace brand from the event’s brand kit. Getting started From a new workspace to a screen that plays: the setup checklist, and quick starts that make a news feed story, a TV channel story, or headlines followed by a few minutes of TV — ordinary stories and scenes you can change afterwards. Venues and the call sheet The places screens are in (each with its time zone), what a stage plays whenever nothing is booked, and the call sheet: stories booked on stages for time windows. A take or a rule still wins over a booking. Stages The screens, projectors and video walls in your workspace. Read them to find the stage_id a session plays on. Channels TV channels imported from your provider’s playlists (M3U / XSPF), and each stage’s current channel. Switching a stage’s channel changes only the video that shows “the stage’s channel” — the rest of the slide stays. Presenters The people who present live on your screens. Import them from a speaker portal or CRM with your own ref, keep them in sync with an idempotent upsert, and upload their photos. Sessions A session is one dated presenter slot in a room: a remote speaker joins through a personal link and appears on every screen of the room (or the screens chosen, or one stage). Plan sessions from your agenda tool, hand out presenter links and schedule rehearsals. Going live and finishing stay with the operator in the app. Engagements Live engagements: the sticky notes, questions and polaroid walls guests post to from their phones. Any system can post into one like a guest — a photo booth, a kiosk, an app — through the same moderation, and get a signed take-home link for each photo or video. Live polls (multiple choice, word clouds, ratings) are prepared and run under /engagements/{id}/polls and /polls/{poll_id}. The posting flow: POST /engagements/{id}/uploads, PUT the file to the upload_url, then POST /engagements/{id}/posts; poll GET /engagements/{id}/posts/{post_id} until share_ready before you show the take-home QR code. Collections and feeds Collections hold the records templates fill from; a feed (RSS, Atom, JSON Feed or a news website) keeps one up to date. Each feed item is a record with title, summary, image, date, time, author, category, link and feed_title. Write records yourself under Records. Records The words your screens show. A template scene has blanks (title, speaker, …); a record fills them for one ref, and every scene that plays that ref shows its values. Change a record and open screens update within seconds. **How records are organized.** There are no collections to create: a workspace has one set of records and the ref is the address. The parts between slashes work like folders — your-event/session/b-14, your-event/room/hall-b, cafeteria/menu/monday — so pick a pattern and list one branch with GET /records?prefix=your-event/session/. A record has no fixed fields: its values are the blanks it fills. **Create a record** with POST /records, change it with PATCH /records/{ref}, and sync many at once with POST /records/batch. Stories A story is what a stage plays: chapters (scenes) in order, looping. A chapter can roll a template through a collection, play for a set time (two minutes of TV after the news), or play only at certain times of day. Put a story on a stage with PUT /stages/{id}/idle or POST /bookings. Scenes and shots A SCENE is a named piece of a story; a SHOT is one timed composition inside it. A shot’s timeline is the whole authoring model — canvas, tracks, blocks, audio, rules — the same shape the editor saves, so a timeline read here can be edited and put straight back. **The loop is read, edit, PUT.** GET /shots/{id}, change what you want, PUT /shots/{id}. There is no per-block route on purpose: a block only means something inside its timeline, and one whole-shot write is simpler and cannot interleave with another. **Reads are forgiving, writes are strict.** A screen must never break on a stored shot, so reading drops what it cannot use; writing instead tells you — an unknown block kind or an unknown rule sensor is a 400 naming it. GET /named-conditions/sensors lists the sensors; block kinds come from the app. Named conditions A meaning given a name once — "between_talks", "doors_open" — that any rule on any block can then refer to by key. Change what it means here and every screen using it follows. Conditions are built from SENSORS (the clock, the date, a data value, the live session); GET /named-conditions/sensors lists them with their operators, so you never have to guess. This is configuration, not content: a named condition says what something *means*, never what a screen shows. Releases In a workspace that publishes manually, screens play the newest release, never the working copies. Publish the working copies as a release (now, at a time, or to some stages first), read the history, and roll back — a rollback is a new release equal to an old one. Live operations and data records never wait for a release. Assets The media library: images, video and audio your scenes use. Upload a file and use its id in the app; list the library to find what is there.

Base URL

Every endpoint lives under one versioned base URL:

Base URL
https://www.wunderframe.com/api/v1

The version is part of the path. Additions — new endpoints, new optional fields — arrive in v1 without notice, so ignore fields you do not know. A breaking change would become /api/v2, and v1 keeps working.

Authentication

Every call carries an API key in the Authorization header. A key belongs to one workspace, so there is no workspace parameter: the key decides where your calls act.

  1. Create a key

    As the workspace owner, open Settings → Integrations, name the key after the system that will use it (“Speaker portal”), tick the scopes it needs and create it.

  2. Copy it now

    The key is shown once and looks like wfi.<id>.<secret>. Wunderframe does not keep the secret, so store it in your secret manager or an environment variable right away.

  3. Make your first call

    Send it as a bearer token. This lists the first five presenters:

curl "https://www.wunderframe.com/api/v1/presenters?limit=5" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"

Scopes

A key can only do what its scopes allow. A call outside them answers 403 forbidden and names the missing scope. Give each system its own key with the fewest scopes it needs.

ScopeAllows
dataCreate, read, update and delete records, collections and their feeds.
presentersImport, update and delete presenters and their photos.
sessions:readRead sessions and their schedule.
sessionsPlan sessions, create invite links and rehearsals.
assetsUpload and list images, video and audio.
rulesRead and write the named conditions rules refer to.
authoringRead and write stories and their chapters, scenes and their shot timelines.
channelsRead the channel line-up and switch a stage’s channel.
publishingPublish releases, read the history and roll back.
stagesMake venues, choose what a stage plays, book stories and read the setup checklist.
engagements:readList live engagements, their rooms and whether they take posts.
engagements:writePost photos, videos and notes into a live engagement like a guest (a photo booth, a kiosk), get the take-home link, and prepare and run live polls.
showsBuild an event’s shows and their cues from its agenda (POST /events/apply).
brandSet the workspace brand: the accent color and the logo guest pages and screens share.

Keys created before scopes existed have data.

Refs and idempotent upserts

Presenters, sessions and records can carry a ref: your own id for them, like speakerportal/1234. A ref is 1 to 200 letters, digits and . _ : / -, starts with a letter or digit, and is unique in the workspace. Slashes are fine, also in URLs: /presenters/by-ref/speakerportal/1234.

With refs you never need to store Wunderframe’s ids. PUT …/by-ref/{ref} creates the item when the ref is new (201) and updates it otherwise (200). Sending the same request twice gives the same result, so a sync that fails halfway can simply run again.

curl -X PUT "https://www.wunderframe.com/api/v1/presenters/by-ref/speakerportal/1234" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Ada Lovelace",
  "title": "Head of Research"
}'

Bulk endpoints such as importing presenters and updating many records take up to 500 items per call.

Errors

Successful calls answer 200, 201 for something created, or 204 with no body. Every error has the same shape, with a message that is safe to show to people:

400 Bad Request
{
  "error": {
    "code": "invalid",
    "message": "\"name\" is longer than 200 characters.",
    "details": { "field": "name" }
  }
}
StatusCodeWhat to do
400invalidFix the request. details.field names the field when there is one.
401unauthorizedThe key is missing, mistyped or revoked.
403forbiddenThe key lacks a scope (details.required), or the role is read-only.
404not_foundNothing with that id or ref in this workspace.
409conflictThe change clashes with the current state — for example deleting a presenter who is in an unfinished session.
413too_largeThe body or file is too big for this endpoint.
415unsupportedThe Content-Type is not accepted here.
423closedThe live engagement takes no posts right now.
429rate_limitedSlow down and retry after a pause.
500internalSomething failed on our side. Retry later.
503unavailableA service the call needs is briefly unavailable. Retry later.

Paging

List endpoints return a page of items and a next_cursor. Pass limit (1–200, default 50) and, for the next page, cursor set to the previous next_cursor. The last page has next_cursor: null. Treat cursors as opaque strings.

A page
{
  "items": [ { "id": "wkFRBzMq6POZ_qYJHvgQf", "name": "Ada Lovelace" } ],
  "next_cursor": "eyJzIjo1MH0"
}
Read every page
let cursor = null;
do {
  const url = new URL('https://www.wunderframe.com/api/v1/presenters');
  url.searchParams.set('limit', '200');
  if (cursor) url.searchParams.set('cursor', cursor);

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.WUNDERFRAME_API_KEY}` }
  });
  const page = await res.json();
  for (const presenter of page.items) console.log(presenter.name);
  cursor = page.next_cursor;
} while (cursor);

Times

Send a time as epoch milliseconds (1791468000000) or ISO 8601 ("2026-10-08T14:00:00Z") — both are accepted. Responses give epoch milliseconds, and session times come with an ISO twin in UTC: starts_at and starts_at_iso. Include a time zone in ISO strings; the start of a session is an exact moment, not a wall-clock time.

Changes show up live

Every write through the API notifies open apps and screens the same way an edit in the app does. A new speaker title reaches the lower third on screen within seconds — there is nothing to refresh.

Limits and good citizenship

  • Bulk endpoints take up to 500 items; split larger syncs into several calls.
  • A platform-wide rate limit protects the service. On 429, wait a few seconds and retry with backoff.
  • Use refs and, where a list offers it, updated_since, rather than re-sending everything on every run.
  • Going live and finishing a session stay with operators in the app and presenters on their page: the API plans sessions, it does not run the desk.

OpenAPI document

The complete reference is available as an OpenAPI 3.1 document at /api/v1/openapi.json. It is public and contains no data. The reference pages of these docs are generated from the same document, so the two always agree.