{"openapi":"3.1.0","info":{"title":"Wunderframe API","version":"1.0.0","description":"Import presenters, plan live sessions, post into live engagements and feed the words and pictures on your screens from any system.","contact":{"name":"Wunderframe","url":"https://www.wunderframe.com/docs"}},"servers":[{"url":"https://www.wunderframe.com/api/v1"}],"security":[{"apiKey":[]}],"tags":[{"name":"Event sync","description":"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.","x-order":2},{"name":"Getting started","description":"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.","x-order":3},{"name":"Venues and the call sheet","description":"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.","x-order":4},{"name":"Stages","description":"The screens, projectors and video walls in your workspace. Read them to find the `stage_id` a session plays on.","x-order":5},{"name":"Channels","description":"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.","x-order":6},{"name":"Presenters","description":"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.","x-order":10},{"name":"Sessions","description":"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.","x-order":20},{"name":"Engagements","description":"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.","x-order":22},{"name":"Collections and feeds","description":"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.","x-order":29},{"name":"Records","description":"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.\n\n**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.\n\n**Create a record** with `POST /records`, change it with `PATCH /records/{ref}`, and sync many at once with `POST /records/batch`.","x-order":30},{"name":"Stories","description":"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.","x-order":33},{"name":"Scenes and shots","description":"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.\n\n**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.\n\n**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.","x-order":34},{"name":"Named conditions","description":"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.","x-order":35},{"name":"Releases","description":"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.","x-order":38},{"name":"Assets","description":"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.","x-order":40}],"paths":{"/events/apply":{"post":{"operationId":"applyEvent","summary":"Apply an event","description":"The event’s whole program for one day (`ref` without a slash; one ref per day of a multi-day event). Rooms, speakers and sessions are addressed by your keys (`[A-Za-z0-9_-]`, up to 64), so a second call with the same keys updates and a session that is gone keeps its record (see `notes`). Sessions take optional `track`, `track_color` (#RRGGBB), `studio_path` and `extra` — translated columns `<field>_<de|en|fr|it>` written into the agenda record. `language`: de, en, fr or it. `dry_run: true` writes nothing and returns what would change. Needs `authoring`, `data`, `presenters`, `sessions` and `shows`.","tags":["Event sync"],"x-scope":"authoring","x-example":{"event":{"ref":"st-0123456789abcdef","title":"Digital Forum","date":"2026-11-05","time_zone":"Europe/Zurich","language":"de","rooms":[{"key":"a1b2c3d4e5f60718","name":"Saal A","label":"EG"}],"speakers":[{"key":"9f8e7d6c5b4a3210","name":"Ada Lovelace","title":"Head of Research","organization":"Analytical Engines Ltd","photo_url":"https://example.com/ada.jpg"}],"sessions":[{"key":"0f1e2d3c4b5a6978","title":"Keynote","room":"all","start":"09:00","end":"09:45","kind":"plenary","speakers":["9f8e7d6c5b4a3210"],"track":"Digital","track_color":"#1fb6ff","takeaway":"Where AI meets the venue","studio_path":"/events/digital-forum/agenda/keynote","extra":{"title_fr":"Discours d’ouverture","title_en":"Keynote"}}]},"dry_run":true,"origin":"studio"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["event"],"properties":{"event":{"type":"object","description":"The EventPlan (see the description)."},"dry_run":{"type":"boolean","default":false},"origin":{"type":"string","enum":["studio","api"]}},"example":{"event":{"ref":"st-0123456789abcdef","title":"Digital Forum","date":"2026-11-05","time_zone":"Europe/Zurich","language":"de","rooms":[{"key":"a1b2c3d4e5f60718","name":"Saal A","label":"EG"}],"speakers":[{"key":"9f8e7d6c5b4a3210","name":"Ada Lovelace","title":"Head of Research","organization":"Analytical Engines Ltd","photo_url":"https://example.com/ada.jpg"}],"sessions":[{"key":"0f1e2d3c4b5a6978","title":"Keynote","room":"all","start":"09:00","end":"09:45","kind":"plenary","speakers":["9f8e7d6c5b4a3210"],"track":"Digital","track_color":"#1fb6ff","takeaway":"Where AI meets the venue","studio_path":"/events/digital-forum/agenda/keynote","extra":{"title_fr":"Discours d’ouverture","title_en":"Keynote"}}]},"dry_run":true,"origin":"studio"}}}}},"responses":{"200":{"description":"What was (or would be) made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventApplyResult"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/brand":{"put":{"operationId":"setBrand","summary":"Set the brand","description":"The workspace brand guest pages and screens share. `primary_color` (or `accent_color`) becomes the accent, with the words on it picked for contrast; `logo_url` (public https) is fetched once as the logo for light backgrounds. What is not sent stays.","tags":["Event sync"],"x-scope":"brand","x-example":{"name":"Digital Forum","primary_color":"#e4007c","accent_color":"#1fb6ff","logo_url":"https://example.com/logo.png"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"primary_color":{"type":"string","pattern":"^#[0-9a-fA-F]{3,6}$"},"accent_color":{"type":"string","pattern":"^#[0-9a-fA-F]{3,6}$"},"logo_url":{"type":"string","format":"uri"}},"example":{"name":"Digital Forum","primary_color":"#e4007c","logo_url":"https://example.com/logo.png"}}}}},"responses":{"200":{"description":"The brand as saved, and contrast warnings.","content":{"application/json":{"schema":{"type":"object","properties":{"brand":{"$ref":"#/components/schemas/Brand"},"warnings":{"type":"array","items":{"type":"string"}}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/connect/token":{"post":{"operationId":"exchangeConnectCode","summary":"Exchange a connect code","description":"After a workspace owner approved your site at `/connect/studio?redirect_uri=…&state=…&code_challenge=…` (PKCE S256), exchange the one-minute code for an API key. No API key needed: the code and its verifier are the credential.","tags":["Event sync"],"x-scope":"none","security":[],"x-example":{"code":"wfc_…","code_verifier":"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk","redirect_uri":"https://studio.example.com/settings/wunderframe"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","code_verifier","redirect_uri"],"properties":{"code":{"type":"string"},"code_verifier":{"type":"string"},"redirect_uri":{"type":"string","format":"uri"}},"example":{"code":"wfc_…","code_verifier":"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk","redirect_uri":"https://studio.example.com/settings/wunderframe"}}}}},"responses":{"200":{"description":"The key, shown once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectToken"}}}},"400":{"$ref":"#/components/responses/E400"}}}},"/setup":{"get":{"operationId":"getSetup","summary":"Read the setup checklist","description":"What the workspace still needs before a screen plays, in order: a venue, a stage, a connected screen, a story, something on the screen; TV channels are optional.","tags":["Getting started"],"x-scope":"stages","responses":{"200":{"description":"The checklist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Setup"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/quick-starts/news":{"post":{"operationId":"quickStartNews","summary":"News from a feed","description":"A collection fed by the feed (read now, then every 15 minutes), a Headline template and a story that rolls through the newest items, `per_pass` (default 5) each round. Needs `authoring` and `data`.","tags":["Getting started"],"x-scope":"authoring","x-example":{"url":"https://www.srf.ch/news/bnf/rss/1646","name":"News","per_pass":5},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","description":"RSS, Atom, JSON Feed — or a news website."},"name":{"type":"string"},"per_pass":{"type":"integer","minimum":1,"maximum":20},"stage_id":{"type":"string","description":"Optional: put the story on this stage (its story whenever nothing is booked). Needs the `stages` scope too."}},"example":{"url":"https://www.srf.ch/news/bnf/rss/1646","name":"News","per_pass":5}}}}},"responses":{"201":{"description":"What was made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuickStartResult"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/quick-starts/tv":{"post":{"operationId":"quickStartTv","summary":"A TV channel","description":"A scene with the channel full screen, its name and a clock, alone in a story — it plays and stays, never restarting.","tags":["Getting started"],"x-scope":"authoring","x-example":{"channel_id":"stage","default_channel_id":"ch_srf1","title":"TV"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["channel_id"],"properties":{"channel_id":{"type":"string","description":"A channel id (GET /channels), or `\"stage\"` for the stage’s channel (PUT /stages/{id}/channel switches it)."},"default_channel_id":{"type":"string","description":"With \"stage\": shown until the stage’s channel is set."},"title":{"type":"string"},"stage_id":{"type":"string","description":"Optional: put the story on this stage (its story whenever nothing is booked). Needs the `stages` scope too."}},"example":{"channel_id":"stage","default_channel_id":"ch_srf1","title":"TV"}}}}},"responses":{"201":{"description":"What was made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuickStartResult"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/quick-starts/news-then-tv":{"post":{"operationId":"quickStartNewsThenTv","summary":"News, then TV","description":"The news feed and Headline template, the TV scene, and a story: `per_pass` headlines, then the channel for `tv_minutes` (default 2), round and round. Change the TV time later with PATCH /chapters/{id} `play_for_ms`. Needs `authoring` and `data`.","tags":["Getting started"],"x-scope":"authoring","x-example":{"url":"https://www.srf.ch/news/bnf/rss/1646","channel_id":"ch_srf1","tv_minutes":2,"per_pass":5},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","channel_id"],"properties":{"url":{"type":"string"},"channel_id":{"type":"string","description":"A channel id (GET /channels), or `\"stage\"` for the stage’s channel (PUT /stages/{id}/channel switches it)."},"default_channel_id":{"type":"string"},"name":{"type":"string"},"tv_minutes":{"type":"number","minimum":0.5},"per_pass":{"type":"integer","minimum":1,"maximum":20},"stage_id":{"type":"string","description":"Optional: put the story on this stage (its story whenever nothing is booked). Needs the `stages` scope too."}},"example":{"url":"https://www.srf.ch/news/bnf/rss/1646","channel_id":"ch_srf1","tv_minutes":2,"per_pass":5}}}}},"responses":{"201":{"description":"What was made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuickStartResult"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/quick-starts/attract":{"post":{"operationId":"quickStartAttract","summary":"Someone in front of the screen","description":"The stage plays `normal` all day and `attract` while someone stands close to it: its camera is turned on (MediaPipe face detection on the screen; nothing is recorded), `normal` becomes its all-day story and the rule goes first in its list. A lobby: news, then a welcome while someone is there; a TV: a channel, then the news. `distance`: close ≈ 1 m (default), near ≈ 2 m, room ≈ 4 m; `leave_s`: keep playing after they leave (default 10). Needs `authoring`, `data` and `stages`.","tags":["Getting started"],"x-scope":"authoring","x-example":{"stage_id":"stg_lobby","normal":{"kind":"news","url":"https://www.srf.ch/news/bnf/rss/1646"},"attract":{"kind":"welcome","headline":"Welcome","line":"Reception is on your left"},"distance":"close"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["stage_id","normal","attract"],"properties":{"stage_id":{"type":"string","description":"A single screen (not a wall)."},"normal":{"type":"object","required":["kind"],"description":"`{ kind: \"news\", url, name? }` · `{ kind: \"tv\", channel_id, default_channel_id? }` · `{ kind: \"story\", story_id }` · `{ kind: \"welcome\", headline, line? }`.","properties":{"kind":{"type":"string","enum":["news","tv","story","welcome"]},"url":{"type":"string"},"name":{"type":"string"},"channel_id":{"type":"string","description":"A channel id (GET /channels), or `\"stage\"` for the stage’s channel (PUT /stages/{id}/channel switches it)."},"default_channel_id":{"type":"string"},"story_id":{"type":"string"},"headline":{"type":"string"},"line":{"type":"string"}}},"attract":{"type":"object","required":["kind"],"description":"`{ kind: \"news\", url, name? }` · `{ kind: \"tv\", channel_id, default_channel_id? }` · `{ kind: \"story\", story_id }` · `{ kind: \"welcome\", headline, line? }`.","properties":{"kind":{"type":"string","enum":["news","tv","story","welcome"]},"url":{"type":"string"},"name":{"type":"string"},"channel_id":{"type":"string","description":"A channel id (GET /channels), or `\"stage\"` for the stage’s channel (PUT /stages/{id}/channel switches it)."},"default_channel_id":{"type":"string"},"story_id":{"type":"string"},"headline":{"type":"string"},"line":{"type":"string"}}},"distance":{"type":"string","enum":["close","near","room"]},"leave_s":{"type":"number","minimum":1,"maximum":600}},"example":{"stage_id":"stg_lobby","normal":{"kind":"news","url":"https://www.srf.ch/news/bnf/rss/1646"},"attract":{"kind":"welcome","headline":"Welcome","line":"Reception is on your left"},"distance":"close"}}}}},"responses":{"201":{"description":"What was made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuickStartResult"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/venues":{"get":{"operationId":"listVenues","summary":"List venues","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of venues.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Venue"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"createVenue","summary":"Create a venue","tags":["Venues and the call sheet"],"x-scope":"stages","x-example":{"name":"Hotel Belvedere","timezone":"Europe/Zurich"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"timezone":{"type":"string"},"address":{"type":"string"}},"example":{"name":"Hotel Belvedere","timezone":"Europe/Zurich"}}}}},"responses":{"201":{"description":"The venue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Venue"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/stages/{id}/idle":{"put":{"operationId":"setStageIdle","summary":"Choose what a stage plays when nothing is booked","description":"A story it plays all day outside bookings, or `null` for the idle card with the venue’s name and clock. Its screens hear it at once.","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"x-example":{"story_id":"sto_news_tv"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["story_id"],"properties":{"story_id":{"type":["string","null"]}},"example":{"story_id":"sto_news_tv"}}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object","properties":{"stage_id":{"type":"string"},"idle":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/stages/{id}/presence":{"put":{"operationId":"setStagePresence","summary":"Notice people in front of a stage’s screen","description":"Turns the stage’s camera on or off for presence rules (a stage rule with sensor `presence`: `near`, `not_near`, or `near_for` with `{ seconds }`). `distance` is how close a face must be to count: `close` ≈ 1 m, `near` ≈ 2 m, `room` ≈ 4 m. `leave_s`: keep playing this long after they leave (default 10); `arrive_s`: seen this long before they count (default 1). Fields left out keep their value. Nothing is recorded — the screen only knows someone is there.","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"x-example":{"enabled":true,"distance":"close","leave_s":10},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"},"distance":{"type":"string","enum":["close","near","room"]},"leave_s":{"type":"number","minimum":1,"maximum":600},"arrive_s":{"type":"number","minimum":0,"maximum":30}},"example":{"enabled":true,"distance":"close","leave_s":10}}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object","properties":{"stage_id":{"type":"string"},"presence":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/stages/{id}/calibration":{"get":{"operationId":"getStageCalibration","summary":"A stage’s screen calibration","description":"What the calibration wizard measured on the screen (spec 18 §5b): the face distance where visitors stand, the comfort zone a relaxed hand moves in (the active box), its reach in palms, and the pinch thresholds of this camera. Null = the defaults.","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The calibration.","content":{"application/json":{"schema":{"type":"object","properties":{"stage_id":{"type":"string"},"calibration":{"type":"object","properties":{"version":{"type":"number"},"at":{"type":"number","description":"Saved at (epoch ms)."},"by":{"type":"string","enum":["editor","screen","api"]},"camera":{"type":["string","null"]},"aspect":{"type":"number"},"face":{"type":["object","null"],"description":"{ share, min_face }: the face’s share of the frame width where visitors stand, and the least that counts."},"box":{"type":["object","null"],"description":"{ top, right, bottom, left }: the comfort zone, mirrored frame fractions (each 0–0.9)."},"reach":{"type":["object","null"],"description":"{ x, y, palm }: the comfort zone in palms."},"pinch":{"type":["object","null"],"description":"{ open_gap, closed_gap, close, open }: palms."},"test":{"type":["object","null"],"description":"{ hits, targets, ms }"}}}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"setStageCalibration","summary":"Set a stage’s screen calibration","description":"Stores a calibration (as the wizard makes it); a part that makes no sense is dropped. Usually made on the screen with *Calibrate this screen* instead. Its screens hear it at once.","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"x-example":{"calibration":{"face":{"share":0.18},"box":{"top":0.55,"right":0.3,"bottom":0.02,"left":0.28},"reach":{"x":3.1,"y":2.6,"palm":0.14},"pinch":{"open_gap":1.1,"closed_gap":0.12,"close":0.46,"open":0.71}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["calibration"],"properties":{"calibration":{"type":"object"}},"example":{"calibration":{"face":{"share":0.18},"box":{"top":0.55,"right":0.3,"bottom":0.02,"left":0.28},"reach":{"x":3.1,"y":2.6,"palm":0.14},"pinch":{"open_gap":1.1,"closed_gap":0.12,"close":0.46,"open":0.71}}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"stage_id":{"type":"string"},"calibration":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"post":{"operationId":"calibrateStageScreens","summary":"Open the calibration wizard on a stage’s screens","description":"Sends `calibrate` to every browser screen of the stage: the guided wizard opens on the screen (stand where visitors stand, trace a comfortable rectangle, open and pinch three times, a test), in German, English or French. Saving there stores it for the stage. Answers how many screens were asked.","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"x-example":{"action":"calibrate"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["calibrate"]}},"example":{"action":"calibrate"}}}}},"responses":{"200":{"description":"Asked.","content":{"application/json":{"schema":{"type":"object","properties":{"stage_id":{"type":"string"},"screens":{"type":"number"}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"delete":{"operationId":"resetStageCalibration","summary":"Reset a stage’s screen calibration","description":"Back to the defaults. Its screens hear it at once.","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Reset.","content":{"application/json":{"schema":{"type":"object","properties":{"stage_id":{"type":"string"},"calibration":{"type":"null"}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/stages/{id}/booth":{"get":{"operationId":"getStageBooth","summary":"A stage’s photo booth","description":"*Use this screen as* Display only or Photo booth (spec 33 §5): on a Photo booth stage a photo-booth block lets visitors take a polaroid at the screen with hand gestures — consent line, live face framing, countdown, a warm screen flash, Keep or Again, and a take-home QR; the photo is posted to `engagement_id` with `source: \"booth\"` (its moderation and retention apply). Display only: the block shows the phone QR. `flash.tone` warm | neutral | cool sets `kelvin` (3200 / 4500 / 6000 K). Needs a camera and a way to choose on the stage (hands in /interaction; or /presence plus the touch fallback — presence alone gives problem no_interaction), and a photo-booth block in a scene it plays. Fields left out keep their value; its screens hear it at once.","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The booth.","content":{"application/json":{"schema":{"type":"object","properties":{"stage_id":{"type":"string"},"booth":{"type":"object","description":"The settings, plus `has_camera`, `can_choose` (hand gestures or the touch fallback) and `problem` (no_engagement, no_camera, no_interaction — the presence camera alone takes no input; null when it takes pictures)."}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"patch":{"operationId":"setStageBooth","summary":"Use a stage’s screen as a photo booth","description":"*Use this screen as* Display only or Photo booth (spec 33 §5): on a Photo booth stage a photo-booth block lets visitors take a polaroid at the screen with hand gestures — consent line, live face framing, countdown, a warm screen flash, Keep or Again, and a take-home QR; the photo is posted to `engagement_id` with `source: \"booth\"` (its moderation and retention apply). Display only: the block shows the phone QR. `flash.tone` warm | neutral | cool sets `kelvin` (3200 / 4500 / 6000 K). Needs a camera and a way to choose on the stage (hands in /interaction; or /presence plus the touch fallback — presence alone gives problem no_interaction), and a photo-booth block in a scene it plays. Fields left out keep their value; its screens hear it at once.","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"x-example":{"mode":"photobooth","engagement_id":"eng_moments","flash":{"on":true,"tone":"warm","strength":0.85}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["display","photobooth"]},"engagement_id":{"type":["string","null"],"description":"Where booth photos go; required for photobooth."},"countdown_s":{"type":"integer","enum":[3,5]},"flash":{"type":"object","properties":{"on":{"type":"boolean"},"tone":{"type":"string","enum":["warm","neutral","cool"],"description":"3200, 4500 or 6000 K."},"kelvin":{"type":"number","minimum":2700,"maximum":6500},"strength":{"type":"number","minimum":0.3,"maximum":1}}},"retries":{"type":"integer","minimum":0,"maximum":5},"keep_timeout_s":{"type":"integer","minimum":5,"maximum":120},"qr_s":{"type":"integer","minimum":10,"maximum":120},"mirror_preview":{"type":"boolean"}},"example":{"mode":"photobooth","engagement_id":"eng_moments","flash":{"on":true,"tone":"warm","strength":0.85}}}}}},"responses":{"200":{"description":"The booth.","content":{"application/json":{"schema":{"type":"object","properties":{"stage_id":{"type":"string"},"booth":{"type":"object","description":"The settings, plus `has_camera`, `can_choose` (hand gestures or the touch fallback) and `problem` (no_engagement, no_camera, no_interaction — the presence camera alone takes no input; null when it takes pictures)."}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"replaceStageBooth","summary":"Use a stage’s screen as a photo booth (PUT)","description":"*Use this screen as* Display only or Photo booth (spec 33 §5): on a Photo booth stage a photo-booth block lets visitors take a polaroid at the screen with hand gestures — consent line, live face framing, countdown, a warm screen flash, Keep or Again, and a take-home QR; the photo is posted to `engagement_id` with `source: \"booth\"` (its moderation and retention apply). Display only: the block shows the phone QR. `flash.tone` warm | neutral | cool sets `kelvin` (3200 / 4500 / 6000 K). Needs a camera and a way to choose on the stage (hands in /interaction; or /presence plus the touch fallback — presence alone gives problem no_interaction), and a photo-booth block in a scene it plays. Fields left out keep their value; its screens hear it at once.","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"x-example":{"mode":"photobooth","engagement_id":"eng_moments","flash":{"on":true,"tone":"warm","strength":0.85}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["display","photobooth"]},"engagement_id":{"type":["string","null"],"description":"Where booth photos go; required for photobooth."},"countdown_s":{"type":"integer","enum":[3,5]},"flash":{"type":"object","properties":{"on":{"type":"boolean"},"tone":{"type":"string","enum":["warm","neutral","cool"],"description":"3200, 4500 or 6000 K."},"kelvin":{"type":"number","minimum":2700,"maximum":6500},"strength":{"type":"number","minimum":0.3,"maximum":1}}},"retries":{"type":"integer","minimum":0,"maximum":5},"keep_timeout_s":{"type":"integer","minimum":5,"maximum":120},"qr_s":{"type":"integer","minimum":10,"maximum":120},"mirror_preview":{"type":"boolean"}},"example":{"mode":"photobooth","engagement_id":"eng_moments","flash":{"on":true,"tone":"warm","strength":0.85}}}}}},"responses":{"200":{"description":"The booth.","content":{"application/json":{"schema":{"type":"object","properties":{"stage_id":{"type":"string"},"booth":{"type":"object","description":"The settings, plus `has_camera`, `can_choose` (hand gestures or the touch fallback) and `problem` (no_engagement, no_camera, no_interaction — the presence camera alone takes no input; null when it takes pictures)."}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/stages/{id}/interaction":{"put":{"operationId":"setStageInteraction","summary":"Let visitors use a stage’s screen, in its languages","description":"*Visitors can use this screen*: touch, a hand pointer (`click`: pinch, dwell or both), swiping, and `idle_timeout_s` (back to the story after that long without a visitor). `languages`: the languages the screen speaks, ISO 639 codes, the FIRST is the default (`[\"de\",\"en\",\"fr\"]`) — a visitor switches with a language-switch block or a `set_language` button, everything on the screen follows, and the screen returns to the first when the visitor leaves; Live lists and tickers turn through them even without a visitor. `[]` removes them. `guidance`: the stage’s own gesture guide — a drawn hand laid over the screen while someone is there (`show: \"presence\"`, the stage camera; or `\"always_when_idle\"`) and nobody uses it, gone the moment a hand goes up; needs `hand`. Merged field by field over what the stage has. Fields left out keep their value.","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"x-example":{"enabled":true,"touch":true,"languages":["de","en","fr"]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"touch":{"type":"boolean"},"hand":{"type":"boolean"},"click":{"type":"string","enum":["pinch","dwell","both"]},"swipe":{"type":"boolean"},"idle_timeout_s":{"type":"number","minimum":5,"maximum":600},"languages":{"type":["array","null"],"items":{"type":"string"}},"guidance":{"type":"object","properties":{"enabled":{"type":"boolean"},"show":{"type":"string","enum":["presence","always_when_idle"]},"steps":{"type":"array","items":{"type":"string","enum":["raise","point","pinch","dwell","swipe"]},"description":"In order; leave out for the gestures this stage understands."},"style":{"type":"string","enum":["line","solid","glass"]},"backdrop":{"type":"string","enum":["blur","panel","none"]},"palette":{"type":"string","enum":["airport","night","light","paper","custom"]},"colors":{"type":"object","description":"Hex colours for palette custom."},"title":{"type":"string","description":"Deutsch|English|Français; \"\" = no title. Default \"So geht’s|How it works|Comment ça marche\"."},"captions":{"type":"object","description":"Per gesture (raise, point, pinch, dwell, swipe), \"Deutsch|English|Français\"."}}}},"example":{"enabled":true,"touch":true,"languages":["de","en","fr"]}}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object","properties":{"stage_id":{"type":"string"},"interaction":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/bookings":{"get":{"operationId":"listBookings","summary":"List the call sheet","tags":["Venues and the call sheet"],"x-scope":"stages","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of bookings.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Booking"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"createBooking","summary":"Book a story on a stage","description":"Weekly by default (`days` needed); `once` needs `date_start`. The same rules and sentences as the call sheet.","tags":["Venues and the call sheet"],"x-scope":"stages","x-example":{"stage_id":"stg_lobby","story_id":"sto_news_tv","start":"07:00","end":"19:00","days":[1,2,3,4,5]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["stage_id","story_id","start","end"],"properties":{"stage_id":{"type":"string","description":"Stage id or name."},"story_id":{"type":"string","description":"Story id or title."},"start":{"type":"string"},"end":{"type":"string"},"days":{"type":"array","items":{"type":"integer"}},"repeat":{"type":"string"},"date_start":{"type":"string"},"date_end":{"type":"string"}},"example":{"stage_id":"stg_lobby","story_id":"sto_news_tv","start":"07:00","end":"19:00","days":[1,2,3,4,5]}}}}},"responses":{"201":{"description":"The booking.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/stages":{"get":{"operationId":"listStages","summary":"List stages","description":"Every stage in the workspace, in the order they were added.","tags":["Stages"],"x-scope":"sessions:read","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of stages.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Stage"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/channels":{"get":{"operationId":"listChannels","summary":"List channels","description":"Every TV channel, in channel order, with its sources.","tags":["Channels"],"x-scope":"channels","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}},{"name":"group","in":"query","required":false,"description":"Only this group (`group-title`).","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Name contains.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of channels.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Channel"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/stages/{id}/channel":{"get":{"operationId":"getStageChannel","summary":"Get a stage’s channel","tags":["Channels"],"x-scope":"channels","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The stage’s channel now, and why.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StageChannel"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"setStageChannel","summary":"Switch a stage’s channel","description":"Sets the channel every “the stage’s channel” video on this stage shows. It beats a rule until handed back with `channel_id: null`. Refused (403) while a live show someone else controls holds the stage.","tags":["Channels"],"x-scope":"channels","parameters":[{"name":"id","in":"path","required":true,"description":"The stage id.","schema":{"type":"string"}}],"x-example":{"channel_id":"ch_srf2"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["channel_id"],"properties":{"channel_id":{"type":["string","null"]}},"example":{"channel_id":"ch_srf2"}}}}},"responses":{"200":{"description":"The stage’s channel after the change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StageChannel"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/presenters":{"get":{"operationId":"listPresenters","summary":"List presenters","description":"Sorted by name. Filter by your `ref`, search `q` in name, title and organization, or fetch only changes with `updated_since`.","tags":["Presenters"],"x-scope":"presenters","parameters":[{"name":"ref","in":"query","required":false,"description":"Only the presenter with this ref.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Search name, title and organization.","schema":{"type":"string"}},{"name":"updated_since","in":"query","required":false,"description":"Epoch ms: only presenters changed since.","schema":{"type":"integer"}},{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of presenters.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Presenter"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"createPresenter","summary":"Create a presenter","description":"Prefer the upsert by ref for syncs: calling it twice is safe.","tags":["Presenters"],"x-scope":"presenters","x-example":{"ref":"speakerportal/1234","name":"Ada Lovelace","title":"Head of Research","organization":"Analytical Engines Ltd","email":"ada@example.com","bio":"Writes the first programs.","photo_url":"https://example.com/photos/ada.jpg"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresenterInput"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Presenter"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"409":{"$ref":"#/components/responses/E409"},"413":{"$ref":"#/components/responses/E413"},"415":{"$ref":"#/components/responses/E415"}}}},"/presenters/{id}":{"get":{"operationId":"getPresenter","summary":"Get a presenter","tags":["Presenters"],"x-scope":"presenters","parameters":[{"name":"id","in":"path","required":true,"description":"The presenter id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The presenter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Presenter"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"patch":{"operationId":"updatePresenter","summary":"Update a presenter","description":"Only the fields you send change. Send `photo_url` to replace the photo.","tags":["Presenters"],"x-scope":"presenters","x-example":{"title":"Chief Scientist"},"parameters":[{"name":"id","in":"path","required":true,"description":"The presenter id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresenterInput"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Presenter"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"}}},"delete":{"operationId":"deletePresenter","summary":"Delete a presenter","description":"Refused with `409 conflict` while the presenter is in a session that has not finished. Their photo stays in the media library.","tags":["Presenters"],"x-scope":"presenters","parameters":[{"name":"id","in":"path","required":true,"description":"The presenter id.","schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"}}}},"/presenters/by-ref/{ref}":{"get":{"operationId":"getPresenterByRef","summary":"Get a presenter by your ref","tags":["Presenters"],"x-scope":"presenters","parameters":[{"name":"ref","in":"path","required":true,"description":"Your ref. May contain slashes: `/presenters/by-ref/speakerportal/1234`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The presenter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Presenter"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"upsertPresenterByRef","summary":"Create or update a presenter by your ref","description":"The sync endpoint. Creates the presenter when no presenter has this ref (then `name` is required), otherwise updates the fields you send. Safe to repeat.","tags":["Presenters"],"x-scope":"presenters","x-example":{"name":"Ada Lovelace","title":"Head of Research","organization":"Analytical Engines Ltd"},"parameters":[{"name":"ref","in":"path","required":true,"description":"Your ref. May contain slashes.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresenterInput"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Presenter"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Presenter"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"409":{"$ref":"#/components/responses/E409"},"413":{"$ref":"#/components/responses/E413"},"415":{"$ref":"#/components/responses/E415"}}}},"/presenters/import":{"post":{"operationId":"importPresenters","summary":"Import presenters in bulk","description":"Up to 500 upserts by ref in one call. Every item needs a `ref`. A row that fails is reported in `failed` and never stops the others.","tags":["Presenters"],"x-scope":"presenters","x-example":{"items":[{"ref":"speakerportal/1234","name":"Ada Lovelace","organization":"Analytical Engines Ltd"},{"ref":"speakerportal/1235","name":"Grace Hopper","title":"Rear Admiral","photo_url":"https://example.com/grace.jpg"}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","maxItems":500,"items":{"$ref":"#/components/schemas/PresenterInput"}}}}}}},"responses":{"200":{"description":"What happened to each row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresenterImportResult"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/presenters/{id}/photo":{"put":{"operationId":"setPresenterPhoto","summary":"Upload a presenter photo","description":"Send the image itself as the body with `Content-Type: image/jpeg` (or png, webp, gif; ≤ 10 MB) and optionally `X-Filename`, **or** JSON `{ \"url\": \"https://…\" }` for Wunderframe to fetch. The photo is stored in the media library.","tags":["Presenters"],"x-scope":"presenters","x-example":{"url":"https://example.com/photos/ada.jpg"},"parameters":[{"name":"id","in":"path","required":true,"description":"The presenter id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"The presenter with its new photo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Presenter"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"413":{"$ref":"#/components/responses/E413"},"415":{"$ref":"#/components/responses/E415"}}},"delete":{"operationId":"removePresenterPhoto","summary":"Remove a presenter photo","tags":["Presenters"],"x-scope":"presenters","parameters":[{"name":"id","in":"path","required":true,"description":"The presenter id.","schema":{"type":"string"}}],"responses":{"204":{"description":"Removed. The image stays in the media library."},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/sessions":{"get":{"operationId":"listSessions","summary":"List sessions","description":"Sorted by start time. The list shows `presenter_ids`; get one session for its presenters' link state.","tags":["Sessions"],"x-scope":"sessions:read","parameters":[{"name":"from","in":"query","required":false,"description":"Only sessions starting at or after this time (epoch ms or ISO 8601).","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"Only sessions starting before this time (epoch ms or ISO 8601).","schema":{"type":"string"}},{"name":"stage_id","in":"query","required":false,"description":"Only sessions that drive this stage: their first screen or any screen of their room they play on (`stage_ids`).","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"One status, or several separated by commas, e.g. `scheduled,backstage,ready`.","schema":{"type":"string"}},{"name":"ref","in":"query","required":false,"description":"Only the session with this ref.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of sessions.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Session"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/sessions/{id}":{"get":{"operationId":"getSession","summary":"Get a session","description":"With its presenters and whether each has a working link.","tags":["Sessions"],"x-scope":"sessions:read","parameters":[{"name":"id","in":"path","required":true,"description":"The session id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionDetail"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"patch":{"operationId":"updateSession","summary":"Update a session","description":"Only the fields you send change. The room and screens cannot change while the session is live (`409`). Every id you send must exist in the workspace (`400`, with `details.unknown` for presenters).","tags":["Sessions"],"x-scope":"sessions","x-example":{"starts_at":"2026-10-08T14:15:00Z","duration_min":45},"parameters":[{"name":"id","in":"path","required":true,"description":"The session id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionInput"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionDetail"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"}}}},"/sessions/by-ref/{ref}":{"get":{"operationId":"getSessionByRef","summary":"Get a session by your ref","tags":["Sessions"],"x-scope":"sessions:read","parameters":[{"name":"ref","in":"path","required":true,"description":"Your ref. May contain slashes: `/sessions/by-ref/agenda/b-14`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionDetail"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"upsertSessionByRef","summary":"Create or update a session by your ref","description":"The sync endpoint. Creates the session when none has this ref (then `room_id` or `stage_id`, and `starts_at`, are required), otherwise updates the fields you send. Safe to repeat. Presenters can be named by your presenter refs.","tags":["Sessions"],"x-scope":"sessions","x-example":{"title":"The AI-Native Startup","room_id":"room_hallB","starts_at":"2026-10-08T14:00:00Z","duration_min":30,"go_live":"on_ready","go_live_by":"operator","after_finish":"next_cue","presenter_refs":["speakerportal/1234","speakerportal/1235"]},"parameters":[{"name":"ref","in":"path","required":true,"description":"Your ref. May contain slashes.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionInput"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionDetail"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionDetail"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"409":{"$ref":"#/components/responses/E409"}}}},"/sessions/{id}/invites":{"post":{"operationId":"createSessionInvite","summary":"Create a presenter link","description":"Makes the personal link a presenter joins with. The link is returned **once** — Wunderframe does not keep it — so pass it on straight away.\n\nIf the presenter already has a working link the call is refused with `409`; send `\"regenerate\": true` to replace it (the old link stops working).\n\nThe presenter must be on the session. Presenter links need a one-time setup by the workspace owner: until the owner has created a first link in the app, this returns `409`.","tags":["Sessions"],"x-scope":"sessions","x-example":{"presenter_id":"wkFRBzMq6POZ_qYJHvgQf"},"parameters":[{"name":"id","in":"path","required":true,"description":"The session id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["presenter_id"],"properties":{"presenter_id":{"type":"string"},"regenerate":{"type":"boolean","default":false,"description":"Replace a working link with a new one."}}}}}},"responses":{"201":{"description":"The link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteLink"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"503":{"$ref":"#/components/responses/E503"}}}},"/sessions/{id}/rehearsals":{"post":{"operationId":"createSessionRehearsal","summary":"Schedule a rehearsal","description":"Adds a rehearsal slot in which presenters can join and test without going on air. Omit `starts_at` to rehearse **now**.\n\nReturns a new link for every presenter on the session who has no working link yet. Refused with `409` once the session has finished or was canceled.","tags":["Sessions"],"x-scope":"sessions","x-example":{"duration_min":30,"note":"Tech check"},"parameters":[{"name":"id","in":"path","required":true,"description":"The session id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["duration_min"],"properties":{"starts_at":{"oneOf":[{"type":"integer","description":"Epoch milliseconds"},{"type":"string","format":"date-time","description":"ISO 8601"}],"description":"When it starts. Omit for now."},"duration_min":{"type":"integer","description":"Minutes: 5–480 for a scheduled rehearsal, 10–240 for one starting now (clamped)."},"note":{"type":"string","maxLength":200}}}}}},"responses":{"201":{"description":"The rehearsal and new links.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RehearsalResult"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"503":{"$ref":"#/components/responses/E503"}}}},"/sessions/{id}/feedback":{"get":{"operationId":"getSessionFeedback","summary":"Get a session’s feedback","description":"What the guests answered in the session’s survey: each question’s result (averages and their spread, NPS, percentages), the text answers newest first — the ones the auto-check held back marked `flagged` — and the feedback link. `?format=csv` answers every response as CSV. No participant ever appears.","tags":["Sessions"],"x-scope":"sessions:read","parameters":[{"name":"id","in":"path","required":true,"description":"The session id.","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"The results.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionFeedback"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/shows/{id}/feedback":{"get":{"operationId":"getShowFeedback","summary":"Get an event’s feedback","description":"Every session of the event (show) with its answers, overall rating, speaker ratings and NPS — the comparison organizers sort.","tags":["Sessions"],"x-scope":"sessions:read","parameters":[{"name":"id","in":"path","required":true,"description":"The show (event) id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The sessions compared.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShowFeedback"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/feedback/form":{"get":{"operationId":"getFeedbackForm","summary":"Get a feedback form","description":"The survey a scope asks: `scope=workspace` (the default every session gets), `scope=show&id=…` or `scope=session&id=…`. `own` is null when the scope inherits; `inherited` is what it gets then (session → event → workspace → the standard form).","tags":["Sessions"],"x-scope":"sessions:read","parameters":[{"name":"scope","in":"query","required":false,"description":"workspace, show or session.","schema":{"type":"string","enum":["workspace","show","session"]}},{"name":"id","in":"query","required":false,"description":"The show or session id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The form.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackFormView"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"setFeedbackForm","summary":"Set a feedback form","description":"Give the scope its own form. Only the fields you send change; the rest comes from what it has, else what it inherits. At most 8 questions; `enabled: false` switches feedback off for the scope.","tags":["Sessions"],"x-scope":"sessions","x-example":{"questions":[{"kind":"rating","text":"How was this session overall?","required":true},{"kind":"rating","text":"How was {speaker}?","per_speaker":true},{"kind":"text","text":"What will you take away?"}],"timing":{"at_end":true,"during":false,"link":true,"open_days":7}},"parameters":[{"name":"scope","in":"query","required":false,"description":"workspace, show or session.","schema":{"type":"string","enum":["workspace","show","session"]}},{"name":"id","in":"query","required":false,"description":"The show or session id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackForm"}}}},"responses":{"200":{"description":"The form.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackFormView"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"delete":{"operationId":"clearFeedbackForm","summary":"Drop a feedback form","description":"The scope inherits again (a session its event’s, an event the workspace’s, the workspace the standard form).","tags":["Sessions"],"x-scope":"sessions","parameters":[{"name":"scope","in":"query","required":false,"description":"workspace, show or session.","schema":{"type":"string","enum":["workspace","show","session"]}},{"name":"id","in":"query","required":false,"description":"The show or session id.","schema":{"type":"string"}}],"responses":{"200":{"description":"What it inherits now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackFormView"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/engagements/{id}/quizzes":{"get":{"operationId":"listQuizzes","summary":"List an engagement’s quizzes","description":"Its quizzes with their rounds, questions and right answers, and the games played (newest first).","tags":["Engagements"],"x-scope":"engagements:read","parameters":[{"name":"id","in":"path","required":true,"description":"The engagement id (from `GET /engagements`).","schema":{"type":"string"}}],"responses":{"200":{"description":"The quizzes and games.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Quiz"}},"games":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"quiz_id":{"type":"string"},"title":{"type":"string"},"phase":{"type":"string"},"started_at":{"type":"integer"},"ended_at":{"type":["integer","null"]}}}}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}},"post":{"operationId":"createQuiz","summary":"Create a quiz","description":"From rounds of questions, or from a spreadsheet (`csv`: rows it cannot read come back in `problems`, the rest is imported).","tags":["Engagements"],"x-scope":"engagements:write","x-example":{"title":"Friday quiz night","settings":{"mode":"teams","leaderboard":"round"},"rounds":[{"title":"Warm-up","questions":[{"kind":"choice","text":"Capital of Switzerland?","options":["Zurich","Bern","Geneva"],"answer":{"index":1}},{"kind":"number","text":"Year of the Moon landing?","answer":{"number":1969,"tolerance":2},"unit":"year"}]}]},"parameters":[{"name":"id","in":"path","required":true,"description":"The engagement id (from `GET /engagements`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizInput"}}}},"responses":{"201":{"description":"The quiz.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quiz"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}}},"/quizzes/{quiz_id}":{"get":{"operationId":"getQuiz","summary":"Get a quiz","tags":["Engagements"],"x-scope":"engagements:read","parameters":[{"name":"quiz_id","in":"path","required":true,"description":"The quiz id (from `GET /engagements/{id}/quizzes`).","schema":{"type":"string"}}],"responses":{"200":{"description":"The quiz.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quiz"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}},"patch":{"operationId":"updateQuiz","summary":"Update a quiz","description":"Only the parts you send change (`csv` replaces the rounds). A game already running keeps the quiz as it was when it started.","tags":["Engagements"],"x-scope":"engagements:write","x-example":{"title":"Friday quiz night — week 2","settings":{"auto_advance":true}},"parameters":[{"name":"quiz_id","in":"path","required":true,"description":"The quiz id (from `GET /engagements/{id}/quizzes`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizInput"}}}},"responses":{"200":{"description":"The quiz.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quiz"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}},"delete":{"operationId":"deleteQuiz","summary":"Delete a quiz","description":"Its games keep their results. Refused (`409`) while a game of it runs.","tags":["Engagements"],"x-scope":"engagements:write","parameters":[{"name":"quiz_id","in":"path","required":true,"description":"The quiz id (from `GET /engagements/{id}/quizzes`).","schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"}}}},"/quizzes/{quiz_id}/games":{"post":{"operationId":"startQuizGame","summary":"Start a game","description":"A lobby: guests join on the engagement’s phone page, the screens show the QR code and the names flowing in. A game running on the engagement ends first. Never start one nobody asked for.","tags":["Engagements"],"x-scope":"engagements:write","parameters":[{"name":"quiz_id","in":"path","required":true,"description":"The quiz id (from `GET /engagements/{id}/quizzes`).","schema":{"type":"string"}}],"responses":{"201":{"description":"The game.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizGame"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"}}}},"/quiz-games/{game_id}":{"get":{"operationId":"getQuizGameResults","summary":"Get a game’s results","description":"The leaderboard and each question’s result (the right answer, counts, the fastest). `?format=csv` answers the final leaderboard as CSV.","tags":["Engagements"],"x-scope":"engagements:read","parameters":[{"name":"game_id","in":"path","required":true,"description":"The game id (from `POST /quizzes/{quiz_id}/games`).","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"The results.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizResults"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}}},"/quiz-games/{game_id}/{action}":{"post":{"operationId":"runQuizGame","summary":"Run a game","description":"Like the Quizmaster: `begin` (the first question), `lock` (stop answers early), `reveal` (the right answer and the scores), `leaderboard`, `next` (the next question, or the podium after the last), `podium`, `end`, and `auto_on` / `auto_off` (the game runs by itself). A question locks by itself when its time is up. `409` when the move does not fit where the game is.","tags":["Engagements"],"x-scope":"engagements:write","parameters":[{"name":"game_id","in":"path","required":true,"description":"The game id (from `POST /quizzes/{quiz_id}/games`).","schema":{"type":"string"}},{"name":"action","in":"path","required":true,"schema":{"type":"string","enum":["begin","lock","reveal","leaderboard","next","podium","end","auto_on","auto_off"]}}],"responses":{"200":{"description":"The game as it is now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizGame"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"}}}},"/engagements/{id}/polls":{"get":{"operationId":"listPolls","summary":"List polls","description":"The engagement’s live polls, in the order they were prepared, each with where its run stands.","tags":["Engagements"],"x-scope":"engagements:read","parameters":[{"name":"id","in":"path","required":true,"description":"The engagement id (from `GET /engagements`).","schema":{"type":"string"}}],"responses":{"200":{"description":"The polls.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Poll"}}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}},"post":{"operationId":"createPoll","summary":"Create a poll","description":"A new poll (a draft) at the end of the engagement’s list. Multiple choice takes 2–10 `options` (strings, or `{ id, label }` to keep an id); a rating a `scale`; a word cloud `words_per_guest` (1–3). The engagement needs polls switched on to run it.","tags":["Engagements"],"x-scope":"engagements:write","x-example":{"type":"choice","question":"Which session should run again tomorrow?","options":["The keynote","The workshop"],"results":"on_reveal"},"parameters":[{"name":"id","in":"path","required":true,"description":"The engagement id (from `GET /engagements`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PollInput"}}}},"responses":{"201":{"description":"The new poll.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Poll"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"}}}},"/polls/{poll_id}":{"patch":{"operationId":"updatePoll","summary":"Change a poll","description":"Change its setup; what you leave out stays. A poll that has run keeps its type, its answers’ ids, its scale and word count until it is reset (`409`); its words may change.","tags":["Engagements"],"x-scope":"engagements:write","x-example":{"question":"Which session should run again on Friday?","results":"live"},"parameters":[{"name":"poll_id","in":"path","required":true,"description":"The poll id (from `GET /engagements/{id}/polls`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PollInput"}}}},"responses":{"200":{"description":"The poll.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Poll"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"}}},"delete":{"operationId":"deletePoll","summary":"Delete a poll","description":"The poll and every vote on it. The phones and screens let it go.","tags":["Engagements"],"x-scope":"engagements:write","parameters":[{"name":"poll_id","in":"path","required":true,"description":"The poll id (from `GET /engagements/{id}/polls`).","schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}}},"/polls/{poll_id}/{action}":{"post":{"operationId":"runPoll","summary":"Run a poll","description":"`launch` puts it on the phones and screens (closing the engagement’s live poll first); `close` stops the voting; `reveal` and `hide` show or hide the results of a poll whose `results` is `on_reveal`; `reset` starts a new run from zero votes.","tags":["Engagements"],"x-scope":"engagements:write","parameters":[{"name":"poll_id","in":"path","required":true,"description":"The poll id (from `GET /engagements/{id}/polls`).","schema":{"type":"string"}},{"name":"action","in":"path","required":true,"schema":{"type":"string","enum":["launch","close","reveal","hide","reset"]}}],"responses":{"200":{"description":"The poll after the move.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Poll"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"}}}},"/polls/{poll_id}/results":{"get":{"operationId":"getPollResults","summary":"Get a poll’s results","description":"The current run’s tally — also while it is hidden from the room. `?format=csv` answers a CSV file. Counts only: no participant ever appears.","tags":["Engagements"],"x-scope":"engagements:read","parameters":[{"name":"poll_id","in":"path","required":true,"description":"The poll id (from `GET /engagements/{id}/polls`).","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"The poll and its tally (`null` before its first launch).","content":{"application/json":{"schema":{"type":"object","properties":{"poll":{"$ref":"#/components/schemas/Poll"},"tally":{"oneOf":[{"$ref":"#/components/schemas/PollTally"},{"type":"null"}]}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}}},"/engagements":{"get":{"operationId":"listEngagements","summary":"List engagements","description":"The workspace’s live engagements, newest first, with their state, what they take and their rooms.","tags":["Engagements"],"x-scope":"engagements:read","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of engagements.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Engagement"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"429":{"$ref":"#/components/responses/E429"}}}},"/engagements/{id}":{"get":{"operationId":"getEngagement","summary":"Get an engagement","description":"Whether it takes posts now (`state`), what kinds, its rooms, whether consent is asked and the limits.","tags":["Engagements"],"x-scope":"engagements:read","parameters":[{"name":"id","in":"path","required":true,"description":"The engagement id (from `GET /engagements`).","schema":{"type":"string"}}],"responses":{"200":{"description":"The engagement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Engagement"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}}},"/engagements/{id}/uploads":{"post":{"operationId":"createEngagementUpload","summary":"Get an upload URL","description":"Step 1 of a photo or video post: one signed URL for one file. PUT the file’s bytes to `upload_url` with `headers`, then name `upload_id` in `POST /engagements/{id}/posts`. The file is checked when you post: it must really be the type you named and no larger than `bytes`.","tags":["Engagements"],"x-scope":"engagements:write","x-example":{"kind":"photo","mime":"image/jpeg","bytes":1843200},"parameters":[{"name":"id","in":"path","required":true,"description":"The engagement id (from `GET /engagements`).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngagementUploadInput"}}}},"responses":{"201":{"description":"Where to send the file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngagementUpload"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"413":{"$ref":"#/components/responses/E413"},"415":{"$ref":"#/components/responses/E415"},"423":{"$ref":"#/components/responses/E423"},"429":{"$ref":"#/components/responses/E429"},"503":{"$ref":"#/components/responses/E503"}}}},"/engagements/{id}/posts":{"post":{"operationId":"createEngagementPost","summary":"Post like a guest","description":"A photo, video, note or question, through the same moderation as a guest’s post. It reaches the walls at once when moderation is `open` (a video once it is made small), or waits for the operator. While the engagement is paused, posts are taken but held off the live walls; while it is closed, `423`. Send an `Idempotency-Key` header to retry safely: the same key and body answer the same post for 24 hours (`200`, `Idempotent-Replayed: true`).","tags":["Engagements"],"x-scope":"engagements:write","x-example":{"kind":"photo","upload_id":"u3f9a0c1d2e4b5a6978c0","caption":"Gala night 2026","room":"up82k8","client_label":"Booth 1","consent":true},"parameters":[{"name":"id","in":"path","required":true,"description":"The engagement id (from `GET /engagements`).","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"Any unique string (1–200 printable characters), e.g. a UUID per shot. A retry with the same key and body returns the first post; another body is a `409`.","schema":{"type":"string","maxLength":200}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngagementPostInput"}}}},"responses":{"200":{"description":"A retry under the same `Idempotency-Key`: the first post, as it is now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngagementPost"}}}},"201":{"description":"The post, with its take-home link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngagementPost"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"413":{"$ref":"#/components/responses/E413"},"415":{"$ref":"#/components/responses/E415"},"423":{"$ref":"#/components/responses/E423"},"429":{"$ref":"#/components/responses/E429"},"503":{"$ref":"#/components/responses/E503"}}}},"/engagements/{id}/posts/{post_id}":{"get":{"operationId":"getEngagementPost","summary":"Get a post’s status","description":"Poll this (every 2–3 seconds) until `share_ready` is true, then show `take_home_url` as a QR code. Only posts made with the same API key are found.","tags":["Engagements"],"x-scope":"engagements:write","parameters":[{"name":"id","in":"path","required":true,"description":"The engagement id (from `GET /engagements`).","schema":{"type":"string"}},{"name":"post_id","in":"path","required":true,"description":"The post id `POST /engagements/{id}/posts` returned.","schema":{"type":"string"}}],"responses":{"200":{"description":"The post as it is now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngagementPost"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"}}}},"/collections":{"get":{"operationId":"listCollections","summary":"List collections","tags":["Collections and feeds"],"x-scope":"data","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of collections.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Collection"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"createCollection","summary":"Create a collection","tags":["Collections and feeds"],"x-scope":"data","x-example":{"name":"News"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"prefix":{"type":"string"},"fields":{"type":"array","description":"The field list, in order, with types. Optional; fields also grow as records arrive.","items":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["text","date","time","image","presenters","record"]},"hidden":{"type":"boolean"},"collection":{"type":"string","description":"type record: the id of the collection it links to. A record field named sponsor owns every sponsor_… value; no other field may start with sponsor_."}}}},"title_field":{"type":"string","description":"The field that names a record."}},"example":{"name":"Programme","prefix":"programme/","fields":[{"name":"date","type":"date"},{"name":"time","type":"time"},{"name":"title"},{"name":"speaker","type":"presenters"},{"name":"photo","type":"image"}],"title_field":"title"}}}}},"responses":{"201":{"description":"The collection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Collection"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/collections/{id}/feeds":{"post":{"operationId":"addFeed","summary":"Feed a collection","description":"Read at once, then on `schedule` (`off`, `5m`, `15m` default, `1h`, `1d`).","tags":["Collections and feeds"],"x-scope":"data","parameters":[{"name":"id","in":"path","required":true,"description":"The collection id.","schema":{"type":"string"}}],"x-example":{"url":"https://www.srf.ch/news/bnf/rss/1646","schedule":"15m"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string"},"name":{"type":"string"},"schedule":{"type":"string"}},"example":{"url":"https://www.srf.ch/news/bnf/rss/1646","schedule":"15m"}}}}},"responses":{"201":{"description":"The source, and how many items it read.","content":{"application/json":{"schema":{"type":"object","properties":{"source_id":{"type":"string"},"items":{"type":"integer"},"error":{"type":["string","null"]}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/sources/{id}/sync":{"post":{"operationId":"syncSource","summary":"Read a source again now","tags":["Collections and feeds"],"x-scope":"data","parameters":[{"name":"id","in":"path","required":true,"description":"The source id.","schema":{"type":"string"}}],"responses":{"200":{"description":"How it went.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"integer"},"error":{"type":["string","null"]}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/records":{"get":{"operationId":"listRecords","summary":"List records","description":"Sorted by ref. Use `prefix` to list one group, e.g. every session of an event.","tags":["Records"],"x-scope":"data","parameters":[{"name":"prefix","in":"query","required":false,"description":"Only refs starting with this text, e.g. `your-event/session/`.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of records.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Record"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"createRecord","summary":"Create a record","description":"Creates the record for a new `ref` with its values. Refused with `409 conflict` when a record with that ref already exists — change it with `PATCH /records/{ref}` (merge) or `PUT /records/{ref}` (replace).\n\nEvery scene that plays this ref shows the values straight away. A ref no scene plays yet is fine: its values show as soon as a scene uses it.","tags":["Records"],"x-scope":"data","x-example":{"ref":"your-event/session/b-14","values":{"title":"The AI-Native Startup","speaker":"Ada Lovelace","room":"Hall B"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordCreate"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Record"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"409":{"$ref":"#/components/responses/E409"}}}},"/records/{ref}":{"get":{"operationId":"getRecord","summary":"Get a record","tags":["Records"],"x-scope":"data","parameters":[{"name":"ref","in":"path","required":true,"description":"The record ref. It may contain slashes and is used as-is: `/records/your-event/session/b-14`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Record"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"replaceRecord","summary":"Create or replace a record","description":"The record holds exactly the values you send; blanks you leave out are removed. Creates the record (`201`) when the ref has none yet, so it is safe to repeat.","tags":["Records"],"x-scope":"data","x-example":{"values":{"title":"The AI-Native Startup","speaker":"Ada Lovelace","room":"Hall B"}},"parameters":[{"name":"ref","in":"path","required":true,"description":"The record ref. It may contain slashes and is used as-is: `/records/your-event/session/b-14`.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordValues"}}}},"responses":{"200":{"description":"Replaced.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Record"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Record"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"patch":{"operationId":"updateRecord","summary":"Create or update a record","description":"The values you send change; every other blank keeps its value. Creates the record (`201`) when the ref has none yet.","tags":["Records"],"x-scope":"data","x-example":{"values":{"speaker":"Grace Hopper"}},"parameters":[{"name":"ref","in":"path","required":true,"description":"The record ref. It may contain slashes and is used as-is: `/records/your-event/session/b-14`.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordValues"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Record"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Record"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"delete":{"operationId":"deleteRecord","summary":"Delete a record","description":"Scenes that played the record fall back to their own field values.","tags":["Records"],"x-scope":"data","parameters":[{"name":"ref","in":"path","required":true,"description":"The record ref. It may contain slashes and is used as-is: `/records/your-event/session/b-14`.","schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted."},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/records/batch":{"post":{"operationId":"batchRecords","summary":"Update many records","description":"Up to 500 items in one call. Each item's `fields` are merged into the record for its `ref`. If no scene plays the ref yet and `template` names a template scene, a filled scene is created from it.\n\nThe whole call is refused with `400` if any item is invalid, naming the first bad one.\n\nUse it for syncs; to create or change a single record, use `POST /records` or `PUT`/`PATCH /records/{ref}`.","tags":["Records"],"x-scope":"data","x-example":{"items":[{"ref":"your-event/session/b-14","fields":{"title":"The AI-Native Startup","speaker":"Ada Lovelace"}},{"ref":"your-event/session/c-01","fields":{"title":"Closing keynote","speaker":"Grace Hopper"},"template":"your-event/session-template","title":"17:00 · Closing keynote"}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","maxItems":500,"items":{"$ref":"#/components/schemas/FieldsItem"}}}}}}},"responses":{"200":{"description":"What happened.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldsResult"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/stories":{"get":{"operationId":"listStories","summary":"List stories","tags":["Stories"],"x-scope":"authoring","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of stories.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Story"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"createStory","summary":"Create a story","tags":["Stories"],"x-scope":"authoring","x-example":{"title":"News and TV"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title"],"properties":{"title":{"type":"string"},"description":{"type":"string"}},"example":{"title":"News and TV"}}}}},"responses":{"201":{"description":"The story.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Story"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/stories/{id}":{"get":{"operationId":"getStory","summary":"Get a story with its chapters","tags":["Stories"],"x-scope":"authoring","parameters":[{"name":"id","in":"path","required":true,"description":"The story id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The story.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryWithChapters"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/stories/{id}/chapters":{"post":{"operationId":"addChapter","summary":"Add a chapter","description":"Cast a scene into the story (at the end, or at `index`) with optional settings — `{ roll: { collection_id, per_pass } }` rolls a template; `{ play_for_ms: 120000 }` plays it for two minutes.","tags":["Stories"],"x-scope":"authoring","parameters":[{"name":"id","in":"path","required":true,"description":"The story id.","schema":{"type":"string"}}],"x-example":{"scene_id":"scn_tv","settings":{"play_for_ms":120000}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scene_id"],"properties":{"scene_id":{"type":"string"},"settings":{"type":"object","description":"Only the given settings change. `play_for_ms`: the chapter plays exactly this long — its last moment holds, or it is cut (not on a rolling chapter). `shot_duration_ms`: every moment’s hold. `roll`: `{ collection_id, per_pass, order: { by: \"added\"|\"field\"|\"collection\", field, direction } }` plays the template once per record, per_pass each round; for advertising add `weight_field` (share of voice), `start_field` / `end_field` (a date window, venue time), `duration_field` (seconds per record, 3–120) and `daypart_field` (daypart names) — a roll for the same collection changes only the keys given. `pick`: a session slot. `plays_during`: `{ daypart_ids, days }`. `fills_gaps`, `start_shot_id`. `detail`: `{ collection_id }` — the chapter never plays in the loop; a visitor opens it by choosing a record of that collection (an agenda board row with `open_detail`, or a row of a collection Live list with `props.open_detail`), filled with that record. One per collection per story.","properties":{"play_for_ms":{"type":["integer","null"],"minimum":1000},"shot_duration_ms":{"type":["integer","null"],"minimum":1000},"start_shot_id":{"type":["string","null"]},"roll":{"type":["object","null"],"description":"A roll through a collection (ADR-0020, ADR-0057). Every `*_field` names a field of the records; null or \"\" = not used.","properties":{"collection_id":{"type":"string"},"per_pass":{"type":"integer","minimum":1,"maximum":20,"default":5},"order":{"type":"object","properties":{"by":{"type":"string","enum":["added","field","collection"]},"field":{"type":["string","null"]},"direction":{"type":"string","enum":["desc","asc"]}}},"match":{"type":"object","additionalProperties":{"type":"string"}},"include_stale":{"type":"boolean","default":false},"max_age_h":{"type":["number","null"]},"weight_field":{"type":["string","null"],"description":"Numeric field: share of voice. Missing value = 1, 0 = paused, max 1000."},"start_field":{"type":["string","null"],"description":"Date or datetime field: the record plays from then (venue time)."},"end_field":{"type":["string","null"],"description":"Date or datetime field: the record plays until then; a date alone = through that day."},"duration_field":{"type":["string","null"],"description":"Number field (or m:ss): seconds the record plays, clamped 3–120."},"daypart_field":{"type":["string","null"],"description":"Text field naming dayparts (\"Morning, Evening\"): the record plays only then."}}},"pick":{"type":["object","null"]},"plays_during":{"type":["object","null"]},"fills_gaps":{"type":"boolean"},"detail":{"type":"object","nullable":true,"properties":{"collection_id":{"type":"string"}},"required":["collection_id"]}}},"index":{"type":"integer"}},"example":{"scene_id":"scn_tv","settings":{"play_for_ms":120000}}}}}},"responses":{"201":{"description":"The chapter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Chapter"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/chapters/{id}":{"patch":{"operationId":"updateChapter","summary":"Change a chapter","description":"Its settings (only the given ones change) and its place in the story.","tags":["Stories"],"x-scope":"authoring","parameters":[{"name":"id","in":"path","required":true,"description":"The chapter id.","schema":{"type":"string"}}],"x-example":{"settings":{"play_for_ms":180000},"index":1},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"settings":{"type":"object","description":"Only the given settings change. `play_for_ms`: the chapter plays exactly this long — its last moment holds, or it is cut (not on a rolling chapter). `shot_duration_ms`: every moment’s hold. `roll`: `{ collection_id, per_pass, order: { by: \"added\"|\"field\"|\"collection\", field, direction } }` plays the template once per record, per_pass each round; for advertising add `weight_field` (share of voice), `start_field` / `end_field` (a date window, venue time), `duration_field` (seconds per record, 3–120) and `daypart_field` (daypart names) — a roll for the same collection changes only the keys given. `pick`: a session slot. `plays_during`: `{ daypart_ids, days }`. `fills_gaps`, `start_shot_id`. `detail`: `{ collection_id }` — the chapter never plays in the loop; a visitor opens it by choosing a record of that collection (an agenda board row with `open_detail`, or a row of a collection Live list with `props.open_detail`), filled with that record. One per collection per story.","properties":{"play_for_ms":{"type":["integer","null"],"minimum":1000},"shot_duration_ms":{"type":["integer","null"],"minimum":1000},"start_shot_id":{"type":["string","null"]},"roll":{"type":["object","null"],"description":"A roll through a collection (ADR-0020, ADR-0057). Every `*_field` names a field of the records; null or \"\" = not used.","properties":{"collection_id":{"type":"string"},"per_pass":{"type":"integer","minimum":1,"maximum":20,"default":5},"order":{"type":"object","properties":{"by":{"type":"string","enum":["added","field","collection"]},"field":{"type":["string","null"]},"direction":{"type":"string","enum":["desc","asc"]}}},"match":{"type":"object","additionalProperties":{"type":"string"}},"include_stale":{"type":"boolean","default":false},"max_age_h":{"type":["number","null"]},"weight_field":{"type":["string","null"],"description":"Numeric field: share of voice. Missing value = 1, 0 = paused, max 1000."},"start_field":{"type":["string","null"],"description":"Date or datetime field: the record plays from then (venue time)."},"end_field":{"type":["string","null"],"description":"Date or datetime field: the record plays until then; a date alone = through that day."},"duration_field":{"type":["string","null"],"description":"Number field (or m:ss): seconds the record plays, clamped 3–120."},"daypart_field":{"type":["string","null"],"description":"Text field naming dayparts (\"Morning, Evening\"): the record plays only then."}}},"pick":{"type":["object","null"]},"plays_during":{"type":["object","null"]},"fills_gaps":{"type":"boolean"},"detail":{"type":"object","nullable":true,"properties":{"collection_id":{"type":"string"}},"required":["collection_id"]}}},"index":{"type":"integer"}},"example":{"settings":{"play_for_ms":180000},"index":1}}}}},"responses":{"200":{"description":"The chapter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Chapter"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"delete":{"operationId":"removeChapter","summary":"Remove a chapter","description":"The scene stays in the library.","tags":["Stories"],"x-scope":"authoring","parameters":[{"name":"id","in":"path","required":true,"description":"The chapter id.","schema":{"type":"string"}}],"responses":{"204":{"description":"Removed."},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/block-kinds":{"get":{"operationId":"listBlockKinds","summary":"List block kinds and rule sensors","description":"**Start here when building a shot.** Every kind a block can be, each with a `template` — a new block of that kind exactly as the editor’s palette makes it — so you can see which fields a caption carries versus a still rather than guessing from the `Block` schema, which lists every field that exists for any kind.\n\nThere is deliberately no hand-written “fields this kind uses” list: a kind’s renderer is built from shared components, so such a list would be incomplete the day it was written. The template is generated by the same call the editor makes, so it cannot drift.\n\n`?width=&height=` measures the templates against your canvas (a template’s box is a fraction of it); the default is 1920×1080. `sensors` is the same vocabulary `GET /named-conditions/sensors` returns, included so one call sets you up for both blocks and rules.\n\nMedia kinds (`still`, `clip`) carry no `asset` in their template: upload one through `/assets` and set its id.","tags":["Scenes and shots"],"x-scope":"authoring","parameters":[{"name":"width","in":"query","required":false,"description":"Canvas width to measure templates against.","schema":{"type":"integer"}},{"name":"height","in":"query","required":false,"description":"Canvas height to measure templates against.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The palette and the sensor vocabulary.","content":{"application/json":{"schema":{"type":"object","properties":{"kinds":{"type":"array","items":{"$ref":"#/components/schemas/BlockKind"}},"sensors":{"type":"array","items":{"$ref":"#/components/schemas/SensorReading"}}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/scenes":{"get":{"operationId":"listScenes","summary":"List scenes","description":"The scene library, newest first.","tags":["Scenes and shots"],"x-scope":"authoring","parameters":[{"name":"q","in":"query","required":false,"description":"Search titles.","schema":{"type":"string"}},{"name":"folder","in":"query","required":false,"description":"Only this folder, or `root` for the top level.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of scenes.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Scene"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"createScene","summary":"Create a scene","description":"It is born with one empty shot, exactly as the library’s New scene is — a scene with no shots plays nothing.","tags":["Scenes and shots"],"x-scope":"authoring","x-example":{"title":"Welcome","kind":"Hero"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SceneCreate"}}}},"responses":{"201":{"description":"The new scene, with its shot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scene"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/scenes/{id}":{"get":{"operationId":"getScene","summary":"Get a scene and its shots","description":"A filled scene lists its template’s shots, because those are what it plays.","tags":["Scenes and shots"],"x-scope":"authoring","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The scene’s id."}],"responses":{"200":{"description":"The scene.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scene"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/scenes/{id}/shots":{"post":{"operationId":"addShot","summary":"Add a shot to a scene","description":"Appended at the end. Send a `timeline` to set it up in the same call. Refused for a filled scene: add the shot to its template instead.","tags":["Scenes and shots"],"x-scope":"authoring","x-example":{"duration_ms":8000},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The scene’s id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShotWrite"}}}},"responses":{"201":{"description":"The new shot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Shot"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/shots/{id}":{"get":{"operationId":"getShot","summary":"Get a shot","description":"The whole timeline. Edit it and PUT it back.","tags":["Scenes and shots"],"x-scope":"authoring","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The shot’s id."}],"responses":{"200":{"description":"The shot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Shot"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"putShot","summary":"Replace a shot’s timeline","description":"Send back a timeline you read from `GET /shots/{id}`. Leaving `duration_ms` out re-derives it from the timeline, which is what the editor does. An unknown block kind or rule sensor is refused with its name.","tags":["Scenes and shots"],"x-scope":"authoring","x-example":{"timeline":{"canvas":{"width":1920,"height":1080,"fps":30,"background":"#000000"},"tracks":[{"uuid":"visual-main","label":"Main","kind":"visual","role":"main","order":0},{"uuid":"visual-overlay","label":"Overlay","kind":"visual","role":"overlay","order":1},{"uuid":"audio-voiceover","label":"Voiceover","kind":"audio","role":"voiceover","order":2},{"uuid":"audio-music","label":"Music","kind":"audio","role":"music","order":3}],"blocks":[{"uuid":"caption_mu6nc9x01","kind":"caption","track_id":"visual-overlay","timing":{"in_ms":0,"animation":"fade","anim_ms":700},"transform":{"x":384,"y":459,"w":1152,"h":162,"rotate":0,"fit":"cover","scale":1,"opacity":1},"text":"{title}","font_weight":700,"color":"#ffffff","align":"center","rules":[{"id":"r1","label":"Hide once live","enabled":true,"match":"all","when":[{"id":"c1","sensor":"session","property":"status","op":"is","value":{"status":"live"}}],"set":{"hidden":true},"fade_ms":1200}]}],"audio":[],"groups":[]}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The shot’s id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShotWrite"}}}},"responses":{"200":{"description":"The saved shot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Shot"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/named-conditions":{"get":{"operationId":"listNamedConditions","summary":"List named conditions","description":"Every named condition in the workspace, by key.","tags":["Named conditions"],"x-scope":"rules","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of named conditions.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/NamedCondition"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"createNamedCondition","summary":"Create or replace a named condition","description":"An upsert by `key`, the same as `PUT /named-conditions/{key}` — posting an existing key updates that one rather than making a second with the same name.","tags":["Named conditions"],"x-scope":"rules","x-example":{"key":"friday_evening","label":"Friday evening","match":"all","when":[{"id":"c1","sensor":"clock","op":"after","value":{"time":"18:00"}},{"id":"g1","all":"any","when":[{"id":"c2","sensor":"weekday","op":"in","value":{"days":[5]}},{"id":"c3","sensor":"weekday","op":"in","value":{"days":[6]}}]}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamedCondition"}}}},"responses":{"200":{"description":"The saved named condition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamedCondition"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/named-conditions/sensors":{"get":{"operationId":"listSensors","summary":"List the sensors a condition can read","description":"What `sensor`, `property` and `op` may be, generated from the sensors this deployment actually has — so it cannot drift from what the evaluator accepts. Each reading carries a ready-made example you can paste into `when`.","tags":["Named conditions"],"x-scope":"rules","responses":{"200":{"description":"The sensor vocabulary.","content":{"application/json":{"schema":{"type":"object","properties":{"sensors":{"type":"array","items":{"$ref":"#/components/schemas/SensorReading"}}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/named-conditions/{key}":{"get":{"operationId":"getNamedCondition","summary":"Get a named condition","tags":["Named conditions"],"x-scope":"rules","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"The named condition’s key."}],"responses":{"200":{"description":"The named condition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamedCondition"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"put":{"operationId":"putNamedCondition","summary":"Create or replace a named condition","description":"Idempotent by key, so a system can sync its own vocabulary by replaying the same calls. A `key` in the body must match the path.","tags":["Named conditions"],"x-scope":"rules","x-example":{"label":"Friday evening","match":"all","when":[{"id":"c1","sensor":"clock","op":"after","value":{"time":"18:00"}},{"id":"g1","all":"any","when":[{"id":"c2","sensor":"weekday","op":"in","value":{"days":[5]}},{"id":"c3","sensor":"weekday","op":"in","value":{"days":[6]}}]}]},"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"The named condition’s key."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamedCondition"}}}},"responses":{"200":{"description":"The saved named condition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamedCondition"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"delete":{"operationId":"deleteNamedCondition","summary":"Delete a named condition","description":"A rule still referring to it simply stops holding — it never starts holding instead, so this cannot make a screen change by itself.","tags":["Named conditions"],"x-scope":"rules","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"The named condition’s key."}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/releases":{"get":{"operationId":"listReleases","summary":"List releases","description":"The release history, newest first.","tags":["Releases"],"x-scope":"publishing","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of releases.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Release"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"publish","summary":"Publish","description":"Publish the working copies as a new release. `stage_ids` limits it to those stages (the others keep theirs); `at` (ISO 8601) schedules it — screens download ahead and switch exactly then.","tags":["Releases"],"x-scope":"publishing","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"note":{"type":"string","maxLength":500},"stage_ids":{"type":"array","items":{"type":"string"}},"at":{"type":"string","format":"date-time"}}}}}},"x-example":{"note":"Lunch menu for Friday","at":"2026-09-20T11:00:00+02:00"},"responses":{"201":{"description":"The release.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishResult"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/releases/{id}/rollback":{"post":{"operationId":"rollbackRelease","summary":"Roll back to a release","description":"A new release equal to release `id`; history is never rewritten.","tags":["Releases"],"x-scope":"publishing","parameters":[{"name":"id","in":"path","required":true,"description":"The release number to go back to.","schema":{"type":"string"}}],"responses":{"201":{"description":"The new release.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Release"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/assets/folders":{"get":{"operationId":"listAssetFolders","summary":"List media folders","description":"Flat, each with the `path` from the top level down to it — so a breadcrumb needs no walking.","tags":["Assets"],"x-scope":"assets","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of folders.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Folder"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"createAssetFolder","summary":"Create a media folder","tags":["Assets"],"x-scope":"assets","x-example":{"name":"Sponsors","parent_id":null},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderWrite"}}}},"responses":{"201":{"description":"The new folder.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Folder"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}}},"/assets/folders/{id}":{"get":{"operationId":"getAssetFolder","summary":"Get a media folder","tags":["Assets"],"x-scope":"assets","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The folder’s id."}],"responses":{"200":{"description":"The folder.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Folder"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"patch":{"operationId":"patchAssetFolder","summary":"Rename or move a media folder","description":"Only the fields you send change. A folder cannot be moved into itself or below itself.","tags":["Assets"],"x-scope":"assets","x-example":{"name":"Sponsors 2027"},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The folder’s id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderWrite"}}}},"responses":{"200":{"description":"The folder.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Folder"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"}}},"delete":{"operationId":"deleteAssetFolder","summary":"Delete a media folder","description":"The folder and those below it go; everything filed in them MOVES UP to this folder’s parent. No file is ever deleted by this.","tags":["Assets"],"x-scope":"assets","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The folder’s id."}],"responses":{"200":{"description":"Where the contents went.","content":{"application/json":{"schema":{"type":"object","properties":{"moved_to":{"type":["string","null"]}},"example":{"moved_to":null}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/assets/{id}":{"get":{"operationId":"getAsset","summary":"Get one file","tags":["Assets"],"x-scope":"assets","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The asset’s id."}],"responses":{"200":{"description":"The file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Asset"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}},"patch":{"operationId":"patchAsset","summary":"Rename or file a media item","description":"Renames it, or moves it into a folder (`folder_id: null` for the top level). The BYTES are immutable: blocks reference an asset by id, so replacing a file would silently change every screen using it — upload a new one instead.","tags":["Assets"],"x-scope":"assets","x-example":{"folder_id":"fld_sponsors"},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The asset’s id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"folder_id":{"type":["string","null"]}},"example":{"name":"Backdrop.jpg","folder_id":"fld_sponsors"}}}}},"responses":{"200":{"description":"The file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Asset"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}}}},"/assets":{"get":{"operationId":"listAssets","summary":"List media","description":"Newest first.","tags":["Assets"],"x-scope":"assets","parameters":[{"name":"kind","in":"query","required":false,"description":"Only this kind.","schema":{"type":"string","enum":["image","video","audio"]}},{"name":"q","in":"query","required":false,"description":"Search file names.","schema":{"type":"string"}},{"name":"folder","in":"query","required":false,"description":"Only this folder, or `root` for the top level. Leave it out for the whole library.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size, 1–200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of media.","content":{"application/json":{"schema":{"type":"object","required":["items","next_cursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}}},"post":{"operationId":"uploadAsset","summary":"Upload a file","description":"Send the file itself as the request body, with its `Content-Type` and optionally its name in `X-Filename` (percent-encode non-ASCII names).\n\nAccepted: images (PNG, JPEG, WebP, GIF, SVG) up to 25 MB; video (MP4, WebM, MOV) and audio (MP3, M4A, WAV, OGG) up to 100 MB. Larger video and audio files can be uploaded in the app, which accepts video up to 1 GB.\n\n```\ncurl -X POST \"$BASE/assets\" -H \"Authorization: Bearer $KEY\" \\\n  -H \"Content-Type: image/jpeg\" -H \"X-Filename: backdrop.jpg\" \\\n  --data-binary @backdrop.jpg\n```","tags":["Assets"],"x-scope":"assets","parameters":[{"name":"folder","in":"query","required":false,"description":"File it in this folder. Omit for the top level.","schema":{"type":"string"}},{"name":"X-Filename","in":"header","required":false,"description":"The name shown in the library. Defaults to `Untitled.<ext>`.","schema":{"type":"string","maxLength":200}}],"requestBody":{"required":true,"description":"The file bytes.","content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"image/gif":{"schema":{"type":"string","format":"binary"}},"image/svg+xml":{"schema":{"type":"string","format":"binary"}},"video/mp4":{"schema":{"type":"string","format":"binary"}},"video/webm":{"schema":{"type":"string","format":"binary"}},"video/quicktime":{"schema":{"type":"string","format":"binary"}},"audio/mpeg":{"schema":{"type":"string","format":"binary"}},"audio/mp4":{"schema":{"type":"string","format":"binary"}},"audio/wav":{"schema":{"type":"string","format":"binary"}},"audio/ogg":{"schema":{"type":"string","format":"binary"}}}},"responses":{"201":{"description":"Uploaded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Asset"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"413":{"$ref":"#/components/responses/E413"},"415":{"$ref":"#/components/responses/E415"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"wfi.<id>.<secret>","description":"A Wunderframe API key from Settings → Integrations. Each key has scopes and belongs to one workspace."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["invalid","unauthorized","forbidden","not_found","conflict","too_large","unsupported","closed","rate_limited","unavailable","internal"]},"message":{"type":"string","description":"Human-readable; safe to show."},"details":{"type":"object","additionalProperties":true,"description":"e.g. `{ \"field\": \"name\" }`."}}}},"example":{"error":{"code":"not_found","message":"Presenter not found."}}},"EventApplyResult":{"type":"object","required":["dry_run","counts","steps"],"properties":{"dry_run":{"type":"boolean"},"counts":{"type":"object","properties":{"create":{"type":"integer"},"update":{"type":"integer"},"keep":{"type":"integer"}}},"steps":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string"},"ref":{"type":"string"},"title":{"type":"string"},"action":{"type":"string","enum":["create","update","keep"]},"note":{"type":"string"},"id":{"type":"string"}}}},"clashes":{"type":"array","items":{"type":"object","properties":{"room":{"type":"string"},"a":{"type":"string"},"b":{"type":"string"}}}},"notes":{"type":"array","items":{"type":"string"}},"stages":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"id":{"type":["string","null"]},"player_url":{"type":["string","null"]}}}}},"example":{"dry_run":true,"counts":{"create":9,"update":0,"keep":0},"steps":[{"kind":"record","ref":"st-0123456789abcdef/agenda/0f1e2d3c4b5a6978","title":"Keynote","action":"create"}],"clashes":[],"notes":[],"stages":[]}},"Brand":{"type":"object","properties":{"colors":{"type":"object","additionalProperties":{"type":"string"}},"fonts":{"type":"object","properties":{"heading":{"type":"string"},"body":{"type":"string"}}},"logo_asset_id":{"type":["string","null"]},"logo_on_dark_asset_id":{"type":["string","null"]}},"example":{"colors":{"background":"#0b1020","surface":"#151b33","text":"#ffffff","muted":"#a3a9c2","accent":"#e4007c","accent_text":"#ffffff"},"fonts":{"heading":"outfit","body":"outfit"},"logo_asset_id":"ast_1","logo_on_dark_asset_id":null}},"ConnectToken":{"type":"object","required":["api_key","workspace_id","workspace_name"],"properties":{"api_key":{"type":"string","description":"Shown once: `wfi.<id>.<secret>`, with the connector’s scopes."},"workspace_id":{"type":"string"},"workspace_name":{"type":"string"}},"example":{"api_key":"wfi.0123456789abcdef.…","workspace_id":"ws_team_1a2b3c","workspace_name":"Digital Forum"}},"Setup":{"type":"object","required":["done","steps"],"properties":{"done":{"type":"boolean","description":"A screen is connected and has something to play."},"facts":{"type":"object","description":"The counts the steps read."},"steps":{"type":"array","items":{"type":"object","properties":{"step":{"type":"string","enum":["venue","stage","screen","story","play","channels"]},"title":{"type":"string"},"done":{"type":"boolean"},"optional":{"type":"boolean"},"blocked":{"type":["string","null"],"description":"The step before it is missing."},"what":{"type":"string"}}}}},"example":{"done":false,"facts":{"venues":1,"stages":1,"screens":0,"stories":0,"onScreen":0,"channels":0},"steps":[{"step":"venue","title":"Add a venue","done":true,"optional":false,"blocked":null,"what":"The place your screens are in…"},{"step":"screen","title":"Connect the screen","done":false,"optional":false,"blocked":null,"what":"Open the Wunderframe app on the TV…"}]}},"QuickStartResult":{"type":"object","required":["storyId","scenes"],"properties":{"storyId":{"type":"string","description":"The story made — GET /stories/{id}."},"scenes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"}}},"description":"The designs made, to edit (the headline template, the TV scene)."},"collectionId":{"type":"string","description":"The feed’s collection, when there is one."},"items":{"type":"integer","description":"Feed items read so far."},"syncError":{"type":["string","null"],"description":"Why the feed could not be read yet."},"normalStoryId":{"type":"string","description":"Someone in front: the story the stage plays the rest of the time."},"stageId":{"type":"string","description":"Someone in front: the stage set up — try it in its preview."}},"example":{"storyId":"sto_1","scenes":[{"id":"scn_1","label":"the headline design"}],"collectionId":"col_news","items":40,"syncError":null}},"Venue":{"type":"object","required":["id","name","timezone"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"timezone":{"type":"string","description":"IANA, e.g. Europe/Zurich."},"address":{"type":"string"}},"example":{"id":"ven_belvedere","name":"Hotel Belvedere","timezone":"Europe/Zurich","address":"Bahnhofstrasse 1, Zürich"}},"Booking":{"type":"object","required":["id","stage_id","story_id","start","end"],"properties":{"id":{"type":"string"},"stage_id":{"type":"string"},"story_id":{"type":"string"},"start":{"type":"string","description":"HH:MM, the venue’s clock."},"end":{"type":"string","description":"At or before start = past midnight."},"days":{"type":"array","items":{"type":"integer"},"description":"0 = Sunday … 6 = Saturday."},"repeat":{"type":"string","enum":["once","daily","weekly","biweekly","monthly_weekday","monthly_date","yearly"]},"date_start":{"type":["string","null"]},"date_end":{"type":["string","null"]},"enabled":{"type":"boolean"}},"example":{"id":"bk_1","stage_id":"stg_lobby","story_id":"sto_news_tv","start":"07:00","end":"19:00","days":[1,2,3,4,5],"repeat":"weekly","date_start":null,"date_end":null,"enabled":true}},"Stage":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"location":{"type":"string","description":"The stage's venue name and space, e.g. \"Main Hall · Room A\"."},"width":{"type":"integer","description":"Canvas width in pixels."},"height":{"type":"integer","description":"Canvas height in pixels."},"orientation":{"type":"string","enum":["landscape","portrait","square"]},"plays_audio":{"type":"boolean","description":"Presenter audio plays on this stage."},"idle_story_id":{"type":["string","null"],"description":"The story it plays whenever nothing is booked; null = the idle card. Set it with PUT /stages/{id}/idle."},"languages":{"type":"array","items":{"type":"string"},"description":"The languages the screen speaks, first = default (spec 31); empty = one. Set them with PUT /stages/{id}/interaction."},"presence":{"type":"object","description":"*Notice people in front of the screen* (the camera for presence rules). Set it with PUT /stages/{id}/presence.","properties":{"enabled":{"type":"boolean"},"distance":{"type":"string","enum":["close","near","room"]},"arrive_s":{"type":"number"},"leave_s":{"type":"number"}}}},"example":{"id":"stg_hall_b","name":"Hall B · Main screen","location":"Ground floor","width":1920,"height":1080,"orientation":"landscape","plays_audio":true,"idle_story_id":"sto_news_tv","languages":["de","en","fr"],"presence":{"enabled":true,"distance":"close","arrive_s":1,"leave_s":10}}},"ChannelSource":{"type":"object","required":["transport","url","venue_ids"],"properties":{"transport":{"type":"string","enum":["multicast","hls","dash","rtsp","http"]},"url":{"type":"string"},"venue_ids":{"type":["array","null"],"items":{"type":"string"},"description":"The venues this source works in (a multicast network); `null` = everywhere."}},"example":{"transport":"multicast","url":"udp://@233.50.230.80:5000","venue_ids":["ven_belvedere"]}},"Channel":{"type":"object","required":["id","name","sources"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"number":{"type":["integer","null"]},"group":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"tvg_id":{"type":["string","null"],"description":"The EPG key (`tvg-id`, else `tvg-name`)."},"sources":{"type":"array","items":{"$ref":"#/components/schemas/ChannelSource"}}},"example":{"id":"ch_srf1","name":"SRF 1","number":1,"group":"de","logo_url":"https://api.tv.init7.net/media/logos/1102_SRF1.ch.png","tvg_id":"SRF1.ch","sources":[{"transport":"multicast","url":"udp://@233.50.230.80:5000","venue_ids":["ven_belvedere"]},{"transport":"hls","url":"https://api.tv.init7.net/api/live/?channel=b87a…","venue_ids":null}]}},"StageChannel":{"type":"object","required":["channel_id","by","why"],"properties":{"channel_id":{"type":["string","null"],"description":"Null: no channel set — each video plays its default."},"name":{"type":["string","null"]},"by":{"type":["string","null"],"enum":["operator","rule",null]},"rule":{"type":["string","null"],"description":"The rule’s name when a rule chose it."},"set_by":{"type":["string","null"],"description":"Who set it, for an operator’s pick."},"why":{"type":"string","description":"The desk’s sentence: “Channel: SRF 1 HD · by rule “Match night””."}},"example":{"channel_id":"ch_srf2","name":"SRF zwei","by":"rule","rule":"Match night","set_by":null,"why":"Channel: SRF zwei · by rule “Match night”"}},"Presenter":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","readOnly":true},"ref":{"type":["string","null"],"pattern":"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$","description":"Your own id for the presenter. Unique in the workspace."},"name":{"type":"string","maxLength":200},"title":{"type":"string","maxLength":200,"description":"Shown in the lower third."},"organization":{"type":"string","maxLength":200},"email":{"type":"string","format":"email"},"bio":{"type":"string","maxLength":4000},"photo_asset_id":{"type":["string","null"],"readOnly":true},"photo_url":{"type":["string","null"],"readOnly":true,"description":"A signed URL, valid for about an hour."},"created_at":{"type":"integer","readOnly":true},"updated_at":{"type":"integer","readOnly":true}},"example":{"id":"wkFRBzMq6POZ_qYJHvgQf","ref":"speakerportal/1234","name":"Ada Lovelace","title":"Head of Research","organization":"Analytical Engines Ltd","email":"ada@example.com","bio":"Writes the first programs.","photo_url":"https://…signed…","photo_asset_id":"a1b2c3","created_at":1789372800000,"updated_at":1789372800000}},"PresenterInput":{"type":"object","properties":{"ref":{"type":["string","null"],"pattern":"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$"},"name":{"type":"string","maxLength":200,"description":"Required when creating."},"title":{"type":"string","maxLength":200},"organization":{"type":"string","maxLength":200},"email":{"type":"string","format":"email"},"bio":{"type":"string","maxLength":4000},"photo_url":{"type":"string","format":"uri","description":"A public https image (JPEG, PNG, WebP, GIF, ≤ 10 MB) that Wunderframe fetches and stores as the photo. Sending the same URL again later does not fetch it again, so repeated imports are cheap; use a new URL (or the photo endpoint) to replace the image."}},"example":{"ref":"speakerportal/1234","name":"Ada Lovelace","title":"Head of Research","organization":"Analytical Engines Ltd","email":"ada@example.com","bio":"Writes the first programs.","photo_url":"https://example.com/photos/ada.jpg"}},"PresenterImportResult":{"type":"object","required":["created","updated","failed"],"properties":{"created":{"type":"integer"},"updated":{"type":"integer"},"failed":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"ref":{"type":["string","null"]},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}}},"example":{"created":41,"updated":3,"failed":[{"index":7,"ref":null,"error":{"code":"invalid","message":"\"ref\" is required for import."}}]}},"SurveyQuestion":{"type":"object","example":{"id":"speaker","kind":"rating","text":"How was {speaker}?","required":false,"style":"stars","per_speaker":true},"required":["kind","text"],"description":"One question of a feedback form.","properties":{"id":{"type":"string","description":"Short and stable (answers name it). Made for you when left out."},"kind":{"type":"string","enum":["rating","nps","choice","yes_no","text"]},"text":{"type":"string","maxLength":200,"description":"`{speaker}` is the presenter’s name on a `per_speaker` question."},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"}}},"description":"`choice`: 2–8."},"max_choices":{"type":"integer","minimum":1},"style":{"type":"string","enum":["stars","faces"],"description":"`rating` only."},"per_speaker":{"type":"boolean","description":"`rating`: asked once per presenter of the session."}}},"FeedbackForm":{"type":"object","example":{"enabled":true,"questions":[{"id":"overall","kind":"rating","text":"How was this session overall?","required":true,"style":"stars"}],"timing":{"at_end":true,"during":false,"link":true,"open_days":7},"anonymous":true,"share_with_speakers":"ratings"},"description":"What a feedback form asks and when.","properties":{"enabled":{"type":"boolean","description":"False: no feedback for this scope (and what inherits from it)."},"questions":{"type":"array","maxItems":8,"items":{"$ref":"#/components/schemas/SurveyQuestion"}},"timing":{"type":"object","properties":{"at_end":{"type":"boolean","description":"Phones bring the Feedback tab forward when the session ends."},"during":{"type":"boolean","description":"The tab is there while the session runs."},"link":{"type":"boolean","description":"The feedback link `/f/<key>` works."},"open_days":{"type":"integer","minimum":1,"maximum":60}}},"anonymous":{"type":"boolean","description":"False: guests may add their name."},"share_with_speakers":{"type":"string","enum":["none","ratings","all"]}}},"FeedbackFormView":{"type":"object","example":{"scope":{"kind":"session","id":"Zp4dW1qR8sN2mK7vB3xYc"},"own":null,"inherited":{"from":"workspace","setup":{"enabled":true,"questions":[{"id":"overall","kind":"rating","text":"How was this session overall?","required":true,"style":"stars"}],"timing":{"at_end":true,"during":false,"link":true,"open_days":7},"anonymous":true,"share_with_speakers":"ratings"}}},"properties":{"scope":{"type":"object","properties":{"kind":{"type":"string","enum":["workspace","show","session"]},"id":{"type":"string"}}},"own":{"oneOf":[{"$ref":"#/components/schemas/FeedbackForm"},{"type":"null"}],"description":"Null: it inherits."},"inherited":{"type":"object","properties":{"from":{"type":"string","enum":["show","workspace","stock"]},"setup":{"$ref":"#/components/schemas/FeedbackForm"}}}}},"FeedbackQuestionResult":{"type":"object","example":{"key":"overall","kind":"rating","text":"How was this session overall?","answered":84,"avg":4.4,"counts":[1,2,6,25,50]},"properties":{"key":{"type":"string","description":"The question id, or `<id>:<presenter id>` per speaker."},"kind":{"type":"string"},"text":{"type":"string"},"presenter_id":{"type":"string"},"answered":{"type":"integer"},"avg":{"type":["number","null"]},"counts":{"type":"array","items":{"type":"integer"},"description":"Per value, lowest first."},"nps":{"type":["integer","null"],"description":"Promoters minus detractors, in percent."},"options":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"count":{"type":"integer"},"pct":{"type":"integer"}}}},"texts":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"at":{"type":"integer"},"flagged":{"type":"boolean"}}}}}},"SessionFeedback":{"type":"object","example":{"session":{"id":"Zp4dW1qR8sN2mK7vB3xYc","title":"The AI-Native Startup","show_id":null,"starts_at":1791468000000,"status":"finished"},"speakers":[{"id":"wkFRBzMq6POZ_qYJHvgQf","name":"Ada Lovelace"}],"form":{"from":"workspace","enabled":true,"share_with_speakers":"ratings"},"window":{"phase":"after","open":true,"closes_at":1792074600000},"results":{"responses":84,"questions":[{"key":"overall","kind":"rating","text":"How was this session overall?","answered":84,"avg":4.4,"counts":[1,2,6,25,50]}]},"link":"https://www.wunderframe.com/f/23456789abcd"},"properties":{"session":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"show_id":{"type":["string","null"]},"starts_at":{"type":"integer"},"status":{"type":"string"}}},"speakers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}}},"form":{"type":"object","properties":{"from":{"type":"string","enum":["session","show","workspace","stock"]},"enabled":{"type":"boolean"},"share_with_speakers":{"type":"string"}}},"window":{"type":"object","properties":{"phase":{"type":"string","enum":["before","during","after","closed","off"]},"open":{"type":"boolean"},"closes_at":{"type":"integer"}}},"results":{"type":"object","properties":{"responses":{"type":"integer"},"questions":{"type":"array","items":{"$ref":"#/components/schemas/FeedbackQuestionResult"}}}},"link":{"type":"string","description":"The feedback link guests open without joining."}}},"ShowFeedback":{"type":"object","example":{"show":{"id":"sh_day1","title":"Day one"},"sessions":[{"id":"Zp4dW1qR8sN2mK7vB3xYc","title":"The AI-Native Startup","starts_at":1791468000000,"status":"finished","enabled":true,"responses":84,"overall":4.4,"nps":41,"speakers":[{"presenter_id":"wkFRBzMq6POZ_qYJHvgQf","name":"Ada Lovelace","avg":4.6}]}]},"properties":{"show":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"}}},"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"starts_at":{"type":"integer"},"status":{"type":"string"},"enabled":{"type":"boolean"},"responses":{"type":"integer"},"overall":{"type":["number","null"]},"nps":{"type":["integer","null"]},"speakers":{"type":"array","items":{"type":"object","properties":{"presenter_id":{"type":"string"},"name":{"type":"string"},"avg":{"type":["number","null"]}}}}}}}}},"PlaySource":{"oneOf":[{"type":"object","required":["kind"],"properties":{"kind":{"const":"blank"}}},{"type":"object","required":["kind","story_id"],"properties":{"kind":{"const":"story"},"story_id":{"type":"string"}}}],"description":"What the stage plays after the session when `after_finish` is `source`: a blank screen or a story.","example":{"kind":"story","story_id":"str_lobbyLoop"}},"Rehearsal":{"type":"object","required":["id","starts_at","ends_at"],"properties":{"id":{"type":"string"},"starts_at":{"type":"integer","description":"Epoch ms. Presenter links open 15 minutes earlier."},"starts_at_iso":{"type":["string","null"],"format":"date-time","description":"The same time in ISO 8601 (UTC)."},"ends_at":{"type":"integer","description":"Epoch ms."},"ends_at_iso":{"type":["string","null"],"format":"date-time","description":"The same time in ISO 8601 (UTC)."},"note":{"type":"string","maxLength":200}},"example":{"id":"a1b2c3d4","starts_at":1791381600000,"starts_at_iso":"2026-10-07T14:00:00.000Z","ends_at":1791383400000,"ends_at_iso":"2026-10-07T14:30:00.000Z","note":"Tech check"}},"Session":{"type":"object","required":["id","title","status","stage_id","stage_ids","starts_at","duration_min","go_live","go_live_by","after_finish","presenter_ids","rehearsals"],"properties":{"id":{"type":"string","readOnly":true},"ref":{"type":["string","null"],"pattern":"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$","description":"Your own id for the session. Unique in the workspace."},"title":{"type":"string","maxLength":200},"status":{"type":"string","enum":["scheduled","backstage","ready","live","finished","cancelled"],"readOnly":true,"description":"`scheduled` → `backstage` (a presenter joined) → `ready` → `live` → `finished`, or `cancelled`. Changed by presenters and operators, not through the API."},"room_id":{"type":["string","null"],"description":"The room the session happens in. A session in a room drives every screen of the room (the front and the side screens show the same), unless `screen_ids` narrows it. `null` for a session placed on a stage in no room."},"screen_ids":{"type":["array","null"],"items":{"type":"string"},"description":"The screens of the room it was narrowed to, as chosen. `null` = every screen of the room, including screens added to the room later."},"stage_ids":{"type":"array","items":{"type":"string"},"readOnly":true,"description":"Every stage (screen) the session drives right now, from its room and `screen_ids`. Follows the room when screens join or leave it."},"stage_id":{"type":"string","description":"The first of `stage_ids` — the one stage, for clients written before rooms. A session on several screens has more in `stage_ids`."},"scene_id":{"type":["string","null"],"description":"The scene that frames the presenter on screen."},"intro_scene_id":{"type":["string","null"],"description":"The speaker intro: a scene played once when the session goes live, before the presenter scene."},"show_id":{"type":["string","null"],"description":"The show this session belongs to, if any."},"starts_at":{"type":"integer","description":"Epoch ms."},"starts_at_iso":{"type":["string","null"],"format":"date-time","description":"The same time in ISO 8601 (UTC)."},"ends_at":{"type":"integer","readOnly":true,"description":"Planned end: `starts_at` + `duration_min`. Epoch ms."},"ends_at_iso":{"type":["string","null"],"format":"date-time","description":"The same time in ISO 8601 (UTC)."},"duration_min":{"type":"integer","minimum":1,"maximum":1440},"join_opens_min":{"type":"integer","minimum":0,"maximum":1440,"description":"How many minutes before the start presenters can join backstage."},"go_live":{"type":"string","enum":["on_ready","at_start"],"description":"WHEN the session is due: `on_ready` as soon as a presenter is ready, `at_start` at `starts_at` once someone is ready."},"go_live_by":{"type":"string","enum":["automatic","operator"],"description":"WHO commits it once due: `automatic` — the stage switches by itself; `operator` — the desk is asked and the stage waits for Take. A live show’s controller is asked either way."},"after_finish":{"type":"string","enum":["next_cue","release","source"],"description":"What the stage does after the session: advance the show, return to its schedule, or play `after_source`."},"after_source":{"oneOf":[{"$ref":"#/components/schemas/PlaySource"},{"type":"null"}]},"presenter_ids":{"type":"array","items":{"type":"string"},"description":"In slot order."},"ready_presenter_ids":{"type":"array","items":{"type":"string"},"readOnly":true},"live_presenter_id":{"type":["string","null"],"readOnly":true},"rehearsals":{"type":"array","items":{"$ref":"#/components/schemas/Rehearsal"},"readOnly":true},"went_live_at":{"type":["integer","null"],"description":"Epoch ms, once live."},"went_live_at_iso":{"type":["string","null"],"format":"date-time","description":"The same time in ISO 8601 (UTC)."},"finished_at":{"type":["integer","null"],"description":"Epoch ms, once finished."},"finished_at_iso":{"type":["string","null"],"format":"date-time","description":"The same time in ISO 8601 (UTC)."},"created_at":{"type":"integer","readOnly":true},"updated_at":{"type":"integer","readOnly":true}},"example":{"id":"Zp4dW1qR8sN2mK7vB3xYc","ref":"agenda/b-14","title":"The AI-Native Startup","status":"scheduled","room_id":"room_hallB","screen_ids":null,"stage_ids":["st_hallB","st_hallB_side"],"stage_id":"st_hallB","scene_id":"sc_presenter","intro_scene_id":"sc_speaker_intro","show_id":null,"starts_at":1791468000000,"starts_at_iso":"2026-10-08T14:00:00.000Z","ends_at":1791469800000,"ends_at_iso":"2026-10-08T14:30:00.000Z","duration_min":30,"join_opens_min":30,"go_live":"on_ready","go_live_by":"automatic","after_finish":"next_cue","after_source":null,"presenter_ids":["wkFRBzMq6POZ_qYJHvgQf"],"ready_presenter_ids":[],"live_presenter_id":null,"rehearsals":[],"went_live_at":null,"went_live_at_iso":null,"finished_at":null,"finished_at_iso":null,"created_at":1789372800000,"updated_at":1789372800000}},"SessionDetail":{"allOf":[{"$ref":"#/components/schemas/Session"},{"type":"object","required":["presenters"],"properties":{"presenters":{"type":"array","description":"The session's presenters in slot order, with the state of their link. Links themselves are never returned here.","items":{"type":"object","required":["id","name","invite"],"properties":{"id":{"type":"string"},"ref":{"type":["string","null"]},"name":{"type":"string"},"invite":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["none","active","revoked"],"description":"`none`: no link yet. `active`: a link works. `revoked`: the link was ended."},"last_seen_at":{"type":["integer","null"],"description":"When the presenter last opened the link. Epoch ms."},"last_seen_at_iso":{"type":["string","null"],"format":"date-time","description":"The same time in ISO 8601 (UTC)."}}}}}}}}],"example":{"id":"Zp4dW1qR8sN2mK7vB3xYc","ref":"agenda/b-14","title":"The AI-Native Startup","status":"scheduled","room_id":"room_hallB","screen_ids":null,"stage_ids":["st_hallB","st_hallB_side"],"stage_id":"st_hallB","scene_id":"sc_presenter","intro_scene_id":"sc_speaker_intro","show_id":null,"starts_at":1791468000000,"starts_at_iso":"2026-10-08T14:00:00.000Z","ends_at":1791469800000,"ends_at_iso":"2026-10-08T14:30:00.000Z","duration_min":30,"join_opens_min":30,"go_live":"on_ready","go_live_by":"automatic","after_finish":"next_cue","after_source":null,"presenter_ids":["wkFRBzMq6POZ_qYJHvgQf"],"ready_presenter_ids":[],"live_presenter_id":null,"rehearsals":[],"went_live_at":null,"went_live_at_iso":null,"finished_at":null,"finished_at_iso":null,"created_at":1789372800000,"updated_at":1789372800000,"presenters":[{"id":"wkFRBzMq6POZ_qYJHvgQf","ref":"speakerportal/1234","name":"Ada Lovelace","invite":{"status":"active","last_seen_at":null,"last_seen_at_iso":null}}]}},"SessionInput":{"type":"object","properties":{"ref":{"type":["string","null"],"pattern":"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$","description":"Ignored by the upsert, which takes the ref from the path."},"title":{"type":"string","maxLength":200,"description":"Defaults to \"Untitled session\"."},"room_id":{"type":["string","null"],"description":"The room it happens in: the session drives every screen of the room, the same on each, unless `screen_ids` narrows it. Creating needs `room_id` or `stage_id`. With `stage_id` too, only that screen of the room. Cannot change while the session is live."},"screen_ids":{"type":["array","null"],"maxItems":50,"items":{"type":"string"},"description":"Only these screens (stage ids) of the room; `null` or `[]` = all of them. Alone, it keeps the session in its room and changes the screens. A screen not in the room is refused (`400`)."},"stage_id":{"type":"string","description":"One screen: the session drives exactly this stage (in its room, if it has one). Kept for clients written before rooms; send `room_id` to drive the whole room. Creating needs `room_id` or `stage_id`. Cannot change while the session is live."},"scene_id":{"type":["string","null"]},"intro_scene_id":{"type":["string","null"],"description":"A scene played once when the session goes live. Its blanks get the speaker and the session."},"show_id":{"type":["string","null"]},"starts_at":{"oneOf":[{"type":"integer","description":"Epoch milliseconds"},{"type":"string","format":"date-time","description":"ISO 8601"}],"description":"Required when creating."},"duration_min":{"type":"integer","description":"Minutes, 1–1440 (values outside are clamped). Default 30."},"join_opens_min":{"type":"integer","description":"Minutes, 0–1440 (clamped). Default 30."},"go_live":{"type":"string","enum":["on_ready","at_start"],"description":"Default `on_ready`."},"go_live_by":{"type":"string","enum":["automatic","operator"],"description":"Default `automatic`."},"after_finish":{"type":"string","enum":["next_cue","release","source"],"description":"Default `release`."},"after_source":{"oneOf":[{"$ref":"#/components/schemas/PlaySource"},{"type":"null"}]},"presenter_ids":{"type":"array","maxItems":50,"items":{"type":"string"},"description":"Replaces the presenter list, in slot order. A presenter taken off the session loses their link."},"presenter_refs":{"type":"array","maxItems":50,"items":{"type":"string"},"description":"The same, by your presenter refs. Send this or `presenter_ids`, not both."}},"example":{"title":"The AI-Native Startup","room_id":"room_hallB","starts_at":"2026-10-08T14:00:00Z","duration_min":30,"go_live":"on_ready","go_live_by":"operator","after_finish":"next_cue","presenter_refs":["speakerportal/1234","speakerportal/1235"]}},"InviteLink":{"type":"object","required":["presenter_id","link"],"properties":{"presenter_id":{"type":"string"},"link":{"type":"string","format":"uri","description":"The presenter's personal link. It works as their login, so send it only to them. It is shown this once."}},"example":{"presenter_id":"wkFRBzMq6POZ_qYJHvgQf","link":"https://www.wunderframe.com/p/wfp.…"}},"RehearsalResult":{"type":"object","required":["rehearsal","links"],"properties":{"rehearsal":{"$ref":"#/components/schemas/Rehearsal"},"links":{"type":"object","additionalProperties":{"type":"string","format":"uri"},"description":"Presenter id → a new link, for each presenter who had no working link. Presenters who already have one keep it: it opens the rehearsal too."}},"example":{"rehearsal":{"id":"a1b2c3d4","starts_at":1789372800000,"starts_at_iso":"2026-09-14T08:00:00.000Z","ends_at":1789376400000,"ends_at_iso":"2026-09-14T09:00:00.000Z","note":"Ad-hoc rehearsal"},"links":{"wkFRBzMq6POZ_qYJHvgQf":"https://www.wunderframe.com/p/wfp.…"}}},"QuizQuestion":{"type":"object","required":["kind","text"],"description":"A question with its right answer. `choice`: 2–4 options (A–D) and `answer.option_id` (or `answer.index`, from 0). `true_false`: `answer.value`. `number`: `answer.number`, `answer.tolerance` (how far off still counts) and an optional `unit`. `order`: 3–5 `options` listed IN THE RIGHT ORDER (players get them shuffled), `partial` for points when partly right.","example":{"id":"cap","kind":"choice","text":"Capital of Switzerland?","options":[{"id":"o1","label":"Zurich"},{"id":"o2","label":"Bern"},{"id":"o3","label":"Geneva"}],"answer":{"option_id":"o2"},"points":1000,"time_s":20},"properties":{"id":{"type":"string","description":"Short and stable; made for you when left out."},"kind":{"type":"string","enum":["choice","true_false","number","order"]},"text":{"type":"string","maxLength":200},"image_asset_id":{"type":["string","null"],"description":"A picture round: an image from the media library."},"options":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string","maxLength":80}}}},"answer":{"type":"object","properties":{"option_id":{"type":"string"},"index":{"type":"integer","description":"On input: the right option by position, from 0."},"value":{"type":"boolean"},"number":{"type":"number"},"tolerance":{"type":"number"}}},"unit":{"type":"string","maxLength":16},"points":{"type":"integer","minimum":0,"maximum":5000,"description":"For a right answer at once (default 1000); right at the last moment scores half."},"time_s":{"type":"integer","minimum":5,"maximum":120,"description":"Seconds to answer (default 20)."},"partial":{"type":"boolean"}}},"QuizSettings":{"type":"object","description":"How the quiz is played.","example":{"mode":"teams","captain":false,"leaderboard":"round","streak":true,"auto_advance":false,"read_ms":3000,"reveal_ms":5000,"board_ms":8000},"properties":{"mode":{"type":"string","enum":["solo","teams"]},"captain":{"type":"boolean","description":"Teams: the captain’s answer counts (else the team’s first)."},"leaderboard":{"type":"string","enum":["question","round","end"]},"streak":{"type":"boolean","description":"A bonus for right answers in a row."},"auto_advance":{"type":"boolean","description":"The game runs by itself: reveal, leaderboard, next."},"read_ms":{"type":"integer","minimum":0,"maximum":15000},"reveal_ms":{"type":"integer","minimum":2000,"maximum":30000},"board_ms":{"type":"integer","minimum":3000,"maximum":60000}}},"QuizInput":{"type":"object","description":"A quiz: its title, rounds of questions and settings — or `csv`, a spreadsheet (a header row naming round, question, type, a–e, answer, time, points, unit; only question and answer needed).","example":{"title":"Friday quiz night","rounds":[{"title":"Warm-up","questions":[{"kind":"choice","text":"Capital of Switzerland?","options":["Zurich","Bern","Geneva"],"answer":{"index":1}}]}],"settings":{"mode":"teams"}},"properties":{"title":{"type":"string","maxLength":80},"rounds":{"type":"array","maxItems":12,"items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"questions":{"type":"array","items":{"$ref":"#/components/schemas/QuizQuestion"}}}}},"settings":{"$ref":"#/components/schemas/QuizSettings"},"csv":{"type":"string","description":"A spreadsheet instead of `rounds`."}}},"Quiz":{"type":"object","description":"A quiz, prepared once and played as games.","example":{"id":"qz_7vB3xYcZp4dW1Q2mK","engagement_id":"eng_Q2mK7vB3xYcZp4dW1","title":"Friday quiz night","rounds":[{"id":"r1","title":"Warm-up","questions":[{"id":"cap","kind":"choice","text":"Capital of Switzerland?","options":[{"id":"o1","label":"Zurich"},{"id":"o2","label":"Bern"},{"id":"o3","label":"Geneva"}],"answer":{"option_id":"o2"},"points":1000,"time_s":20}]}],"settings":{"mode":"teams","captain":false,"leaderboard":"round","streak":true,"auto_advance":false,"read_ms":3000,"reveal_ms":5000,"board_ms":8000},"questions":1,"problem":null,"created_at":1791468000000,"updated_at":1791468000000},"properties":{"id":{"type":"string"},"engagement_id":{"type":"string"},"title":{"type":"string"},"rounds":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"questions":{"type":"array","items":{"$ref":"#/components/schemas/QuizQuestion"}}}}},"settings":{"$ref":"#/components/schemas/QuizSettings"},"questions":{"type":"integer"},"problem":{"type":["string","null"],"description":"Why it cannot be played yet."},"problems":{"type":"array","items":{"type":"string"},"description":"A spreadsheet import: the rows that could not be read."},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}}},"QuizGame":{"type":"object","description":"One playing of a quiz. `phase`: lobby, question, locked, reveal, leaderboard, podium, ended. Times are server epoch ms.","example":{"id":"qg_Zp4dW1Q2mK7vB3xYc","quiz_id":"qz_7vB3xYcZp4dW1Q2mK","engagement_id":"eng_Q2mK7vB3xYcZp4dW1","title":"Friday quiz night","phase":"question","q":0,"total":12,"auto":false,"opens_at":1791468003000,"locks_at":1791468023000,"started_at":1791467900000,"ended_at":null},"properties":{"id":{"type":"string"},"quiz_id":{"type":"string"},"engagement_id":{"type":"string"},"title":{"type":"string"},"phase":{"type":"string","enum":["lobby","question","locked","reveal","leaderboard","podium","ended"]},"q":{"type":"integer","description":"The question, counted across rounds from 0; -1 in the lobby."},"total":{"type":"integer"},"auto":{"type":"boolean"},"opens_at":{"type":["integer","null"]},"locks_at":{"type":["integer","null"]},"started_at":{"type":"integer"},"ended_at":{"type":["integer","null"]}}},"QuizResults":{"type":"object","description":"A game’s leaderboard and each question’s result, counted from the answers. No participant ever appears.","example":{"game":{"id":"qg_Zp4dW1Q2mK7vB3xYc","title":"Friday quiz night","mode":"teams","phase":"ended","started_at":1791467900000,"ended_at":1791471500000,"players":38,"teams":9},"leaderboard":[{"id":"tm_1","name":"The Quizzly Bears","score":8450,"correct":10,"streak":3,"rank":1,"prev_rank":2,"last":{"answered":true,"correct":true,"points":910},"players":4}],"questions":[{"index":0,"round":"Warm-up","text":"Capital of Switzerland?","kind":"choice","question_id":"cap","answer":{"option_id":"o2"},"counts":{"o1":11,"o2":25,"o3":2},"answered":38,"right":25,"fastest":{"name":"Ada · The Quizzly Bears","ms":2140}}]},"properties":{"game":{"type":"object"},"leaderboard":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"score":{"type":"integer"},"correct":{"type":"integer"},"rank":{"type":"integer"},"players":{"type":"integer","description":"Teams only."}}}},"questions":{"type":"array","items":{"type":"object"}}}},"Poll":{"type":"object","required":["id","engagement_id","type","question","state"],"properties":{"id":{"type":"string"},"engagement_id":{"type":"string"},"order":{"type":"integer"},"type":{"type":"string","enum":["choice","word_cloud","rating"]},"question":{"type":"string","maxLength":200},"options":{"type":"array","description":"Multiple choice: 2–10 answers. The id never changes when the label does.","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string","maxLength":80}}}},"max_choices":{"type":"integer","description":"1 = one answer; more = up to that many."},"scale":{"type":["object","null"],"description":"A rating: 1–5 or 1–10 as numbers, stars or faces (stars and faces are 1–5).","properties":{"min":{"type":"integer"},"max":{"type":"integer","enum":[5,10]},"style":{"type":"string","enum":["numbers","stars","emoji"]},"labels":{"type":["array","null"],"items":{"type":"string"}}}},"words_per_guest":{"type":"integer","minimum":1,"maximum":3},"results":{"type":"string","enum":["live","on_reveal"],"description":"The room sees the results as they come in, or once revealed."},"phone_results":{"type":"string","enum":["after_vote","on_reveal","never"]},"number_format":{"type":"string","enum":["percent","count","both"]},"allow_change":{"type":"boolean"},"state":{"type":"string","enum":["draft","live","closed"]},"revealed":{"type":"boolean"},"run":{"type":"integer","description":"Bumped by `reset`: earlier votes stop counting."},"launched_at":{"type":["integer","null"]},"closed_at":{"type":["integer","null"]},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}},"example":{"id":"pol_8Hk2mQ4xZ1","engagement_id":"eng_Q2mK7vB3xYcZp4dW1","order":0,"type":"choice","question":"Which session should run again tomorrow?","options":[{"id":"a1b2","label":"The keynote"},{"id":"c3d4","label":"The workshop"}],"max_choices":1,"scale":null,"words_per_guest":1,"results":"on_reveal","phone_results":"after_vote","number_format":"percent","allow_change":true,"state":"live","revealed":false,"run":1,"launched_at":1790150400000,"closed_at":null,"created_at":1790140000000,"updated_at":1790150400000}},"PollInput":{"type":"object","properties":{"type":{"type":"string","enum":["choice","word_cloud","rating"]},"question":{"type":"string","maxLength":200},"options":{"type":"array","items":{"oneOf":[{"type":"string"},{"type":"object"}]}},"max_choices":{"type":"integer"},"scale":{"type":"object"},"words_per_guest":{"type":"integer"},"results":{"type":"string","enum":["live","on_reveal"]},"phone_results":{"type":"string","enum":["after_vote","on_reveal","never"]},"number_format":{"type":"string","enum":["percent","count","both"]},"allow_change":{"type":"boolean"}},"example":{"type":"choice","question":"Which session should run again tomorrow?","options":["The keynote","The workshop"],"results":"on_reveal"}},"PollTally":{"type":"object","properties":{"poll_id":{"type":"string"},"run":{"type":"integer"},"voters":{"type":"integer"},"options":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"count":{"type":"integer"},"pct":{"type":"integer"}}}},"words":{"type":"array","items":{"type":"object","properties":{"word":{"type":"string"},"count":{"type":"integer"}}}},"rating":{"type":"object","properties":{"avg":{"type":["number","null"]},"counts":{"type":"array","items":{"type":"integer"}}}},"at":{"type":"integer","description":"When it was counted (epoch milliseconds)."}},"example":{"poll_id":"pol_8Hk2mQ4xZ1","run":1,"voters":312,"options":[{"id":"a1b2","count":201,"pct":64},{"id":"c3d4","count":111,"pct":36}],"at":1790150460000}},"Engagement":{"type":"object","required":["id","name","key","url","state","open","moderation","accepts","rooms","consent","limits"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"key":{"type":"string","description":"The public key in the guests’ link, `/e/<key>`."},"url":{"type":"string","format":"uri","description":"The phone page guests open."},"state":{"type":"string","enum":["open","paused","closed"],"description":"`open` takes posts; `paused` takes them but holds them off the live walls until it opens again; `closed` refuses them (`423`)."},"open":{"type":"boolean","description":"Posts are taken now (`open` or `paused`)."},"moderation":{"type":"string","enum":["open","approval","auto_check"],"description":"`open`: straight to the walls. `approval`: every post waits for the operator. `auto_check`: clean posts go through, flagged ones wait."},"accepts":{"type":"object","description":"What a post may be now, after the workspace’s rules (videos can be switched off for the whole workspace).","properties":{"note":{"type":"boolean"},"question":{"type":"boolean"},"photo":{"type":"boolean"},"video":{"type":"boolean"}}},"rooms":{"type":"array","description":"Where posts can come from. Send a room’s `key` as `room` on a post so walls set to that room show it.","items":{"type":"object","properties":{"key":{"type":"string","description":"6 characters; never changes when the room is renamed."},"label":{"type":"string"},"url":{"type":"string","format":"uri"}}}},"consent":{"type":"object","description":"When `required`, show the text to the person in their language (`texts.de` / `texts.en` / `texts.fr`; `text` is the German) and send `consent: true` with their post. The workspace default wording exists in all three; an own text is the same in each.","properties":{"required":{"type":"boolean"},"text":{"type":"string"},"texts":{"type":"object","properties":{"de":{"type":"string"},"en":{"type":"string"},"fr":{"type":"string"}}}}},"limits":{"type":"object","description":"The file sizes, video length and text lengths this engagement takes.","properties":{"photo_max_bytes":{"type":"integer"},"video_max_bytes":{"type":"integer"},"video_max_s":{"type":"integer"},"note_max_chars":{"type":"integer"},"question_max_chars":{"type":"integer"},"photo_caption_max_chars":{"type":"integer"},"video_caption_max_chars":{"type":"integer"},"name_max_chars":{"type":"integer"},"client_label_max_chars":{"type":"integer"}}},"scope":{"type":"object","description":"What it belongs to: the workspace, a show or a session.","properties":{"kind":{"type":"string","enum":["workspace","room","show","session"]},"id":{"type":"string"}}},"created_at":{"type":["integer","null"],"description":"Epoch milliseconds."},"availability":{"type":["object","null"],"description":"When guests can take part: why (`always`, `window` = its opening hours, `range` = its dates, `session`, `on_screen`, `manual`, `override`, `shot_setting`), until when it stays as it is, when it opens next, and one sentence in German, English and French (\"Guests can take part now · until 22:00\").","properties":{"reason":{"type":"string"},"until":{"type":["integer","null"],"description":"Epoch milliseconds."},"next_open":{"type":["integer","null"],"description":"Epoch milliseconds."},"sentence":{"type":"object","properties":{"de":{"type":"string"},"en":{"type":"string"},"fr":{"type":"string"}}},"time_zone":{"type":["string","null"]}}},"retention_days":{"type":"integer","description":"Posts and photos are deleted this many days after they are posted."}},"example":{"id":"eng_Q2mK7vB3xYcZp4dW1","name":"Gala night","key":"a6ptt2gre6vu","url":"https://www.wunderframe.com/e/a6ptt2gre6vu","state":"open","open":true,"moderation":"approval","accepts":{"note":true,"question":false,"photo":true,"video":true},"rooms":[{"key":"up82k8","label":"Lobby","url":"https://www.wunderframe.com/e/a6ptt2gre6vu/up82k8"}],"consent":{"required":true,"text":"Ich bin einverstanden, dass mein Beitrag und mein Foto auf den Bildschirmen gezeigt und bis zum Ende der Aufbewahrungsfrist gespeichert werden.","texts":{"de":"Ich bin einverstanden, dass mein Beitrag und mein Foto auf den Bildschirmen gezeigt und bis zum Ende der Aufbewahrungsfrist gespeichert werden.","en":"I agree that my post and photo are shown on the screens and kept until the retention period ends.","fr":"J’accepte que ma publication et ma photo soient affichées sur les écrans et conservées jusqu’à la fin de la durée de conservation."}},"limits":{"photo_max_bytes":8388608,"video_max_bytes":62914560,"video_max_s":15,"note_max_chars":280,"question_max_chars":280,"photo_caption_max_chars":60,"video_caption_max_chars":60,"name_max_chars":40,"client_label_max_chars":40},"scope":{"kind":"workspace","id":"ws_acme"},"created_at":1790150400000}},"EngagementUploadInput":{"type":"object","required":["kind","mime","bytes"],"properties":{"kind":{"type":"string","enum":["photo","video"]},"mime":{"type":"string","description":"A photo: `image/jpeg`, `image/png` or `image/webp`. A video: `video/mp4`, `video/quicktime` or `video/webm`."},"bytes":{"type":"integer","minimum":1,"description":"The file’s exact size. A photo up to 8 MB, a video up to 60 MB."}},"example":{"kind":"photo","mime":"image/jpeg","bytes":1843200}},"EngagementUpload":{"type":"object","required":["upload_id","upload_url","method","headers"],"properties":{"upload_id":{"type":"string","description":"Name it in the post. Good for one post, within an hour."},"upload_url":{"type":"string","format":"uri","description":"Send the file’s bytes here, at once (the URL is short-lived and single-use)."},"method":{"type":"string","description":"Usually `PUT`."},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Send these headers with the upload."}},"example":{"upload_id":"u3f9a0c1d2e4b5a6978c0","upload_url":"https://www.wunderframe.com/_storage/upload/…","method":"PUT","headers":{"Content-Type":"image/jpeg"}}},"EngagementPostInput":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["photo","video","note","question"]},"upload_id":{"type":"string","description":"A photo or video: the `upload_id` its upload returned, after the PUT."},"caption":{"type":"string","description":"A photo’s or video’s caption, or the words of a note or question (`text` is the same field)."},"text":{"type":"string","description":"The same as `caption`."},"name":{"type":"string","maxLength":40,"description":"The person’s name, shown with the post. Leave it out for anonymous."},"room":{"type":"string","description":"A room key (or label) of the engagement. An unknown room is refused."},"client_label":{"type":"string","maxLength":40,"description":"Your own label for where it came from, e.g. `Booth 1`. Stored on the post."},"consent":{"type":"boolean","description":"Required (`true`) when the engagement’s `consent.required`: the person agreed to the consent text."}},"example":{"kind":"photo","upload_id":"u3f9a0c1d2e4b5a6978c0","caption":"Gala night 2026","room":"up82k8","client_label":"Booth 1","consent":true}},"EngagementPost":{"type":"object","required":["post_id","engagement_id","kind","status","share_ready","take_home_url","created_at"],"properties":{"post_id":{"type":"string"},"engagement_id":{"type":"string"},"kind":{"type":"string","enum":["photo","video","note","question"]},"status":{"type":"string","enum":["pending","approved","featured","answered","rejected","hidden"],"description":"`pending` waits for the operator; `approved`, `featured` and `answered` are on the walls; `rejected` and `hidden` are not."},"video_status":{"type":["string","null"],"enum":["processing","ready","failed",null],"description":"A video is made small on the server first; it reaches the walls once `ready`. `null` for other kinds."},"room":{"type":["string","null"],"description":"The room key."},"client_label":{"type":["string","null"]},"share_ready":{"type":"boolean","description":"The take-home copy is made: the take-home page shows it at once. On the free plan it carries the Wunderframe watermark."},"take_home_url":{"type":["string","null"],"format":"uri","description":"A signed link to a page where the person sees their photo or video and can share or save it. Show it as a QR code. For one post, for 24 hours after it was made. `null` for notes and questions, and for a post that was rejected or taken down."},"take_home_expires_at":{"type":["integer","null"],"description":"When the link stops working (epoch milliseconds)."},"source":{"type":"string","enum":["phone","api","booth"],"description":"Where it came from: a guest’s phone, an API client, or a stage set to Photo booth."},"crop":{"type":["object","null"],"description":"The face-aware crop box every screen frames the photo by: 0..1 of the original, x and y the top-left corner. `null` until faces were looked for.","properties":{"x":{"type":"number"},"y":{"type":"number"},"w":{"type":"number"},"h":{"type":"number"}}},"crop_source":{"type":["string","null"],"enum":["faces","booth","center","manual",null]},"keepsake_ready":{"type":"boolean","description":"The branded keepsake polaroid is made; the take-home page shows and shares it."},"keepsake_url":{"type":["string","null"],"format":"uri","description":"Where the person gets the keepsake: the take-home page."},"created_at":{"type":"integer","description":"Epoch milliseconds."}},"example":{"post_id":"pst_7vB3xYcZp4dW1qR8","engagement_id":"eng_Q2mK7vB3xYcZp4dW1","kind":"photo","status":"pending","video_status":null,"room":"up82k8","client_label":"Booth 1","share_ready":false,"take_home_url":"https://www.wunderframe.com/e/a6ptt2gre6vu/p/pst_7vB3xYcZp4dW1qR8?t=sp1x2c.hK3…","take_home_expires_at":1790236800000,"source":"api","crop":{"x":0.12,"y":0,"w":0.75,"h":1},"crop_source":"faces","keepsake_ready":false,"keepsake_url":"https://www.wunderframe.com/e/a6ptt2gre6vu/p/pst_7vB3xYcZp4dW1qR8?t=sp1x2c.hK3…","created_at":1790150400000}},"Collection":{"type":"object","required":["id","name","prefix"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"prefix":{"type":"string","description":"Its records’ refs start with this."},"fields":{"type":"array","items":{"type":"string"}},"field_types":{"type":"object","additionalProperties":{"type":"string","enum":["text","date","time","image","presenters","record"]},"description":"Per field: text, date (YYYY-MM-DD), time (HH:MM), image (asset:<id> or https://…), presenters (write <name>_ids = presenter ids; <name>, <name>_title, <name>_organization and <name>_photo are derived) or record (write <name>_ref = refs of records in the linked collection; <name> = their titles and <name>_<field> for each field of that collection are derived, and kept current)."},"field_links":{"type":"object","additionalProperties":{"type":"string"},"description":"A record field → the id of the collection it links to."},"title_field":{"type":["string","null"]},"template_id":{"type":["string","null"]}},"example":{"id":"col_news","name":"News","prefix":"news/","fields":["title","summary","image"],"template_id":"scn_headline"}},"Record":{"type":"object","required":["ref","values","origin","stale","created_at","updated_at"],"properties":{"ref":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$","description":"Your key for the record, e.g. `your-event/session/b-14`. Slashes group records."},"values":{"type":"object","additionalProperties":{"type":"string","maxLength":2000},"description":"Blank name → text. Blank names start with a letter, then letters, digits or `_`."},"origin":{"type":"string","enum":["hand","api","source","migration"],"description":"Who wrote the values last: `hand` in the app, `api` through this API, `source` a connected data source, `migration` moved from a scene."},"stale":{"type":"boolean","description":"The data source that owns the record no longer contains it."},"created_at":{"type":"integer","description":"Epoch ms."},"updated_at":{"type":"integer","description":"Epoch ms."}},"example":{"ref":"your-event/session/b-14","values":{"title":"The AI-Native Startup","speaker":"Ada Lovelace","room":"Hall B"},"origin":"api","stale":false,"created_at":1789372800000,"updated_at":1789376400000}},"RecordCreate":{"type":"object","required":["ref","values"],"properties":{"ref":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$","description":"Your key for the record. Slashes group records: `your-event/session/b-14`."},"values":{"type":"object","maxProperties":200,"additionalProperties":{"type":["string","null"],"maxLength":2000},"description":"Blank name → text. Blank names start with a letter, then letters, digits or `_`. `null` stores an empty value."}},"example":{"ref":"your-event/session/b-14","values":{"title":"The AI-Native Startup","speaker":"Ada Lovelace","room":"Hall B"}}},"RecordValues":{"type":"object","required":["values"],"properties":{"values":{"type":"object","maxProperties":200,"additionalProperties":{"type":["string","null"],"maxLength":2000},"description":"Blank name → text. `null` stores an empty value."}},"example":{"values":{"title":"The AI-Native Startup","speaker":"Ada Lovelace"}}},"FieldsItem":{"type":"object","required":["ref"],"properties":{"ref":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$"},"fields":{"type":"object","additionalProperties":{"type":["string","number","boolean","null"]},"description":"Blank name → value, merged into the record. Numbers and booleans are stored as text; values are cut at 2000 characters."},"template":{"type":"string","description":"A template scene (its id or its ref). When no scene plays this `ref` yet, a filled scene is created from it."},"title":{"type":"string","maxLength":200,"description":"The title of a scene created from `template`."}}},"FieldsResult":{"type":"object","required":["updated","created","stored","unknown"],"properties":{"updated":{"type":"integer","description":"Items whose record already existed."},"stored":{"type":"integer","description":"Items whose record is new."},"created":{"type":"integer","description":"Filled scenes created from a `template`."},"unknown":{"type":"array","items":{"type":"string"},"description":"Refs that no scene plays yet. Their values are stored all the same and show as soon as a scene uses the ref."}},"example":{"updated":12,"stored":2,"created":1,"unknown":["your-event/session/c-01"]}},"Story":{"type":"object","required":["id","title"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"created_at":{"type":["integer","null"]}},"example":{"id":"sto_news_tv","title":"News and TV","description":"","status":"draft","created_at":1789990000000}},"Chapter":{"type":"object","required":["id","index","scene_id","settings"],"properties":{"id":{"type":"string"},"index":{"type":"integer","description":"0 = first."},"scene_id":{"type":"string"},"scene_title":{"type":["string","null"]},"settings":{"type":"object","description":"Only the given settings change. `play_for_ms`: the chapter plays exactly this long — its last moment holds, or it is cut (not on a rolling chapter). `shot_duration_ms`: every moment’s hold. `roll`: `{ collection_id, per_pass, order: { by: \"added\"|\"field\"|\"collection\", field, direction } }` plays the template once per record, per_pass each round; for advertising add `weight_field` (share of voice), `start_field` / `end_field` (a date window, venue time), `duration_field` (seconds per record, 3–120) and `daypart_field` (daypart names) — a roll for the same collection changes only the keys given. `pick`: a session slot. `plays_during`: `{ daypart_ids, days }`. `fills_gaps`, `start_shot_id`. `detail`: `{ collection_id }` — the chapter never plays in the loop; a visitor opens it by choosing a record of that collection (an agenda board row with `open_detail`, or a row of a collection Live list with `props.open_detail`), filled with that record. One per collection per story.","properties":{"play_for_ms":{"type":["integer","null"],"minimum":1000},"shot_duration_ms":{"type":["integer","null"],"minimum":1000},"start_shot_id":{"type":["string","null"]},"roll":{"type":["object","null"],"description":"A roll through a collection (ADR-0020, ADR-0057). Every `*_field` names a field of the records; null or \"\" = not used.","properties":{"collection_id":{"type":"string"},"per_pass":{"type":"integer","minimum":1,"maximum":20,"default":5},"order":{"type":"object","properties":{"by":{"type":"string","enum":["added","field","collection"]},"field":{"type":["string","null"]},"direction":{"type":"string","enum":["desc","asc"]}}},"match":{"type":"object","additionalProperties":{"type":"string"}},"include_stale":{"type":"boolean","default":false},"max_age_h":{"type":["number","null"]},"weight_field":{"type":["string","null"],"description":"Numeric field: share of voice. Missing value = 1, 0 = paused, max 1000."},"start_field":{"type":["string","null"],"description":"Date or datetime field: the record plays from then (venue time)."},"end_field":{"type":["string","null"],"description":"Date or datetime field: the record plays until then; a date alone = through that day."},"duration_field":{"type":["string","null"],"description":"Number field (or m:ss): seconds the record plays, clamped 3–120."},"daypart_field":{"type":["string","null"],"description":"Text field naming dayparts (\"Morning, Evening\"): the record plays only then."}}},"pick":{"type":["object","null"]},"plays_during":{"type":["object","null"]},"fills_gaps":{"type":"boolean"},"detail":{"type":"object","nullable":true,"properties":{"collection_id":{"type":"string"}},"required":["collection_id"]}}}},"example":{"id":"plc_tv","index":1,"scene_id":"scn_tv","scene_title":"TV","settings":{"play_for_ms":120000,"shot_duration_ms":null,"start_shot_id":null,"roll":null,"pick":null,"plays_during":null,"fills_gaps":false}}},"StoryWithChapters":{"allOf":[{"$ref":"#/components/schemas/Story"},{"type":"object","properties":{"chapters":{"type":"array","items":{"$ref":"#/components/schemas/Chapter"}}}}],"example":{"id":"sto_news_tv","title":"News and TV","description":"","status":"draft","created_at":1789990000000,"chapters":[]}},"Canvas":{"type":"object","description":"The frame everything is positioned inside. `width`/`height` are the composition size, not the screen’s.","properties":{"width":{"type":"integer"},"height":{"type":"integer"},"fps":{"type":"integer","description":"Composition frame rate. 30 unless set."},"background":{"type":"string","description":"CSS color under everything."},"backdrop":{"type":"object","description":"Painted over the background where no block is: `style` of `solid` | `gradient` | `orbs`, with its own colors."}},"example":{"width":1920,"height":1080,"fps":30,"background":"#000000"}},"Track":{"type":"object","description":"A row blocks sit on. VISUAL rows stack back-to-front by `order` — a higher `order` paints in front (ADR-0026) — and AUDIO rows carry sound. A shot starts with Main, Overlay, Voiceover and Music; you rarely need more.","required":["uuid","kind","order"],"properties":{"uuid":{"type":"string","description":"What a block’s `track_id` points at."},"label":{"type":"string"},"kind":{"type":"string","enum":["visual","audio"]},"role":{"type":"string","enum":["main","overlay","voiceover","music","sfx"]},"order":{"type":"integer","description":"Back to front among visual rows."},"hidden":{"type":"boolean"},"locked":{"type":"boolean"},"muted":{"type":"boolean"},"height":{"type":"integer","description":"Editor row height. Cosmetic."}},"example":{"uuid":"visual-overlay","label":"Overlay","kind":"visual","role":"overlay","order":1}},"Transform":{"type":"object","description":"Where the block sits. **x, y, w and h are CANVAS PIXELS**, not fractions — on a 1920×1080 canvas, `x: 384, w: 1152` is a band 60% wide, centered.","required":["x","y"],"properties":{"x":{"type":"number","description":"Left edge, in canvas pixels."},"y":{"type":"number","description":"Top edge, in canvas pixels."},"w":{"type":"number","description":"Width in canvas pixels. Omit for the block’s natural width."},"h":{"type":"number","description":"Height in canvas pixels."},"rotate":{"type":"number","description":"Degrees."},"fit":{"type":"string","enum":["cover","contain"],"description":"How media fills the box."},"scale":{"type":"number","description":"Multiplier, 1 = as laid out."},"opacity":{"type":"number","minimum":0,"maximum":1}},"example":{"x":384,"y":459,"w":1152,"h":162,"rotate":0,"fit":"cover","scale":1,"opacity":1}},"Timing":{"type":"object","description":"When the block is on screen, in ms from the start of the shot. **Leaving `out_ms` empty means “until the end”** — which is what a background or a logo wants, and stays true when the shot gets longer.","required":["in_ms"],"properties":{"in_ms":{"type":"integer"},"out_ms":{"type":"integer","description":"Omit for “to the end of the shot”."},"animation":{"type":"string","description":"Legacy entrance. `transition_in` wins over it."},"anim_ms":{"type":"integer"}},"example":{"in_ms":0,"animation":"fade","anim_ms":700}},"Block":{"type":"object","description":"One thing on screen. `kind` decides which of the optional fields mean anything — a `caption` reads `text`, a `still` reads `asset`, a `presenter` reads `presenter_source`. `GET /scenes` will not tell you the kinds; the app’s block palette and the MCP’s `list_block_kinds` do, and an unknown kind is refused on write.\n\nFields not listed here are dropped on write. Two are COMPOSE-ONLY and can never be stored: `image_url` and `agenda_view` are filled by the server when a screen asks for the shot.","required":["uuid","kind","track_id","timing","transform"],"properties":{"uuid":{"type":"string","description":"Unique within the shot. Keep it stable across edits."},"kind":{"type":"string","description":"e.g. `caption`, `still`, `clip`, `widget`, `presenter`, `agenda`."},"track_id":{"type":"string","description":"A `Track.uuid`. Which row it sits on, and so what it paints in front of."},"timing":{"$ref":"#/components/schemas/Timing"},"transform":{"$ref":"#/components/schemas/Transform"},"label":{"type":"string","description":"Shown in the editor’s layer list."},"hidden":{"type":"boolean"},"locked":{"type":"boolean"},"text":{"type":"string","description":"Caption and widget text. `{blanks}` fill from the record."},"font_size":{"type":"number"},"font_weight":{"type":"number"},"font_family":{"type":"string","description":"`heading`, `body`, a device stack (`system-ui`, `serif`, `monospace`) or any Google Fonts family by name. Screens download a Google family and play it offline."},"color":{"type":"string"},"align":{"type":"string","enum":["left","center","right"]},"backdrop":{"type":"string","enum":["none","shadow","scrim","box"]},"asset":{"type":"string","description":"Clip / still: an asset id from `/assets`. Never a URL or storage key."},"image_blank":{"type":"string","description":"Still: a blank holding a picture URL, filled per record."},"alt":{"type":"string"},"ken_burns":{"type":"string","enum":["none","in","out","pan-left","pan-right"]},"audible":{"type":"boolean","description":"A clip’s own sound, opt-in."},"volume":{"type":"number"},"trim_in_ms":{"type":"integer","description":"Clip: where it starts in the file."},"trim_out_ms":{"type":"integer","description":"Clip with `loop`: the end of the section that repeats (the file’s own time)."},"loop":{"type":"boolean","description":"Repeat for as long as the block runs: a clip its `trim_in_ms`…`trim_out_ms` section (at least 200 ms), a Ken Burns still its drift every `loop_ms`."},"loop_ms":{"type":"integer","description":"A looping Ken Burns still: one drift there and back. Default 8000."},"freeze":{"type":"boolean","description":"Clip: a freeze frame — shows the frame at `trim_in_ms` and never plays."},"keep_running":{"type":"boolean","description":"Clip with a `morph_key`: the same video with the same name and `keep_running` in the next shot is this element, still playing — its box moves, it never restarts."},"widget":{"type":"string","enum":["clock","date","countdown","progress"]},"format":{"type":"string","enum":["short","medium","long"]},"target_at":{"type":"integer","description":"Countdown / progress target, epoch ms."},"target_blank":{"type":"string","description":"Countdown / progress target from a blank; wins over `target_at`. Every screen fills `next_session_at` (its stage’s next session); a session slot fills `starts_at`, `ends_at`, `phase_from`."},"from_at":{"type":"integer","description":"Progress: when it starts filling, epoch ms. Missing = `props.span_min` (default 10) minutes before the target."},"from_blank":{"type":"string","description":"Progress start from a blank; wins over `from_at`."},"presenter_source":{"type":"string","description":"Presenter: camera, shared screen, or both."},"presenter_slot":{"type":"integer","description":"0 = whoever is live; n = the session’s n-th presenter."},"name_label":{"type":"boolean","description":"Presenter: a lower third with name, title and organization."},"mask":{"type":"object","description":"`shape` of `none` | `circle` | `rounded`, with radius, border and shadow."},"transition_in":{"type":"object","description":"`{ kind, ms }` — cut, dissolve, fade, slide-*, scale."},"transition_out":{"type":"object"},"morph_key":{"type":"string","description":"The same name in the next shot is the same thing, moved."},"show_when":{"type":"object","description":"Alias, accepted on write: `{ test: filled }`, `{ test: has|empty, field }` or `{ test: is|is_not, field, value }`. Stored as the equivalent \"Show only when…\" rule — first in `rules`, `id: show_when`, `phase: fill`, `set: { hidden: true }` — and never written back. Older blocks may still carry it; it decides exactly as that rule does."},"agenda":{"type":"object","description":"Agenda board settings (ADR-0023)."},"props":{"type":"object","description":"A block kind’s own settings."},"rules":{"type":"array","items":{"$ref":"#/components/schemas/Rule"},"description":"Sensor-driven changes laid over this block WHILE they hold."},"layer":{"type":"string","enum":["under","over"],"description":"Layouts only: under or over the shot’s own blocks."}},"example":{"uuid":"caption_mu6nc9x01","kind":"caption","track_id":"visual-overlay","timing":{"in_ms":0,"animation":"fade","anim_ms":700},"transform":{"x":384,"y":459,"w":1152,"h":162,"rotate":0,"fit":"cover","scale":1,"opacity":1},"text":"{title}","font_weight":700,"color":"#ffffff","align":"center","rules":[{"id":"r1","label":"Hide once live","enabled":true,"match":"all","when":[{"id":"c1","sensor":"session","property":"status","op":"is","value":{"status":"live"}}],"set":{"hidden":true},"fade_ms":1200}]}},"AudioItem":{"type":"object","description":"A sound on an audio track, placed on the shot’s clock rather than given in/out points like a block.","required":["uuid","track_id","at_ms"],"properties":{"uuid":{"type":"string"},"track_id":{"type":"string","description":"An audio `Track.uuid`."},"asset":{"type":"string","description":"An asset id."},"label":{"type":"string"},"at_ms":{"type":"integer","description":"Where it starts in the shot."},"out_ms":{"type":"integer"},"volume":{"type":"number"},"ducking":{"type":"boolean","description":"Drop under a voiceover."},"fade_in_ms":{"type":"integer"},"fade_out_ms":{"type":"integer"},"trim_in_ms":{"type":"integer","description":"Into the source file."},"trim_out_ms":{"type":"integer","description":"With `loop`: the end of the section that repeats."},"loop":{"type":"boolean","description":"Repeat the `trim_in_ms`…`trim_out_ms` section until `out_ms`."}},"example":{"uuid":"aud_1","track_id":"audio-music","asset":"ast_sting","at_ms":0,"out_ms":8000,"volume":0.6}},"Timeline":{"type":"object","description":"**The shot document.** Canvas, rows, what is on them, sound, and the rules that change it — the same shape the editor saves, read by the same `readTimeline`.\n\n**Always start from a `GET`.** Fields this version does not know are dropped on write, so building one by hand loses whatever you did not think to include; reading, editing and putting back never does.","required":["canvas","tracks","blocks"],"properties":{"canvas":{"$ref":"#/components/schemas/Canvas"},"tracks":{"type":"array","items":{"$ref":"#/components/schemas/Track"}},"blocks":{"type":"array","items":{"$ref":"#/components/schemas/Block"},"description":"Order within a track does not matter; `track_id` and the track’s `order` decide what is in front."},"audio":{"type":"array","items":{"$ref":"#/components/schemas/AudioItem"}},"groups":{"type":"array","items":{"type":"object"},"description":"Editor groupings. A block joins one with `group_id`."},"canvas_rules":{"type":"array","items":{"$ref":"#/components/schemas/Rule"},"description":"Rules on the BACKGROUND — a different backdrop at night, on a date. Applied when the shot is composed."},"safe_area":{"type":"object","description":"Layouts only: a guide rectangle shot content should stay inside."},"fonts":{"type":"object","description":"Heading and body families. A linked shot plays its layout’s unless it sets its own."}},"example":{"canvas":{"width":1920,"height":1080,"fps":30,"background":"#000000"},"tracks":[{"uuid":"visual-main","label":"Main","kind":"visual","role":"main","order":0},{"uuid":"visual-overlay","label":"Overlay","kind":"visual","role":"overlay","order":1},{"uuid":"audio-voiceover","label":"Voiceover","kind":"audio","role":"voiceover","order":2},{"uuid":"audio-music","label":"Music","kind":"audio","role":"music","order":3}],"blocks":[{"uuid":"caption_mu6nc9x01","kind":"caption","track_id":"visual-overlay","timing":{"in_ms":0,"animation":"fade","anim_ms":700},"transform":{"x":384,"y":459,"w":1152,"h":162,"rotate":0,"fit":"cover","scale":1,"opacity":1},"text":"{title}","font_weight":700,"color":"#ffffff","align":"center","rules":[{"id":"r1","label":"Hide once live","enabled":true,"match":"all","when":[{"id":"c1","sensor":"session","property":"status","op":"is","value":{"status":"live"}}],"set":{"hidden":true},"fade_ms":1200}]}],"audio":[],"groups":[]}},"Shot":{"type":"object","required":["id","scene_id","duration_ms","timeline"],"properties":{"id":{"type":"string"},"scene_id":{"type":"string"},"position":{"type":"number","description":"Order within the scene. Fractional, so a shot can be inserted between two."},"duration_ms":{"type":"integer","description":"How long it holds. At least 500."},"layout_id":{"type":["string","null"],"description":"A linked layout whose blocks show behind or over this shot’s."},"layout_opts":{"type":"object","description":"What the shot takes from its layout: `background` \"layout\" (default) or \"own\", `hide` — layout block uuids this shot leaves out (e.g. a logo the shot shows larger itself), `audio` \"layout\" | \"own\" | \"mute\".","properties":{"background":{"type":"string","enum":["layout","own"]},"hide":{"type":"array","items":{"type":"string"}},"audio":{"type":"string","enum":["layout","own","mute"]}}},"timeline":{"$ref":"#/components/schemas/Timeline"},"created_at":{"type":"integer","readOnly":true},"updated_at":{"type":"integer","readOnly":true}},"example":{"id":"sht_a1","scene_id":"scn_welcome","position":1,"duration_ms":8000,"layout_id":null,"timeline":{"canvas":{"width":1920,"height":1080,"fps":30,"background":"#000000"},"tracks":[{"uuid":"visual-main","label":"Main","kind":"visual","role":"main","order":0},{"uuid":"visual-overlay","label":"Overlay","kind":"visual","role":"overlay","order":1},{"uuid":"audio-voiceover","label":"Voiceover","kind":"audio","role":"voiceover","order":2},{"uuid":"audio-music","label":"Music","kind":"audio","role":"music","order":3}],"blocks":[{"uuid":"caption_mu6nc9x01","kind":"caption","track_id":"visual-overlay","timing":{"in_ms":0,"animation":"fade","anim_ms":700},"transform":{"x":384,"y":459,"w":1152,"h":162,"rotate":0,"fit":"cover","scale":1,"opacity":1},"text":"{title}","font_weight":700,"color":"#ffffff","align":"center","rules":[{"id":"r1","label":"Hide once live","enabled":true,"match":"all","when":[{"id":"c1","sensor":"session","property":"status","op":"is","value":{"status":"live"}}],"set":{"hidden":true},"fade_ms":1200}]}],"audio":[],"groups":[]},"created_at":1789372800000,"updated_at":1789372800000}},"ShotWrite":{"type":"object","description":"Only what you send changes.","properties":{"timeline":{"$ref":"#/components/schemas/Timeline"},"duration_ms":{"type":"integer","minimum":500},"layout_id":{"type":["string","null"]},"layout_opts":{"type":"object","description":"What the shot takes from its layout: `background` \"layout\" (default) or \"own\", `hide` — layout block uuids this shot leaves out (e.g. a logo the shot shows larger itself), `audio` \"layout\" | \"own\" | \"mute\".","properties":{"background":{"type":"string","enum":["layout","own"]},"hide":{"type":"array","items":{"type":"string"}},"audio":{"type":"string","enum":["layout","own","mute"]}}}},"example":{"timeline":{"canvas":{"width":1920,"height":1080,"fps":30,"background":"#000000"},"tracks":[{"uuid":"visual-main","label":"Main","kind":"visual","role":"main","order":0},{"uuid":"visual-overlay","label":"Overlay","kind":"visual","role":"overlay","order":1},{"uuid":"audio-voiceover","label":"Voiceover","kind":"audio","role":"voiceover","order":2},{"uuid":"audio-music","label":"Music","kind":"audio","role":"music","order":3}],"blocks":[{"uuid":"caption_mu6nc9x01","kind":"caption","track_id":"visual-overlay","timing":{"in_ms":0,"animation":"fade","anim_ms":700},"transform":{"x":384,"y":459,"w":1152,"h":162,"rotate":0,"fit":"cover","scale":1,"opacity":1},"text":"{title}","font_weight":700,"color":"#ffffff","align":"center","rules":[{"id":"r1","label":"Hide once live","enabled":true,"match":"all","when":[{"id":"c1","sensor":"session","property":"status","op":"is","value":{"status":"live"}}],"set":{"hidden":true},"fade_ms":1200}]}],"audio":[],"groups":[]},"duration_ms":8000}},"Scene":{"type":"object","required":["id","title"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"kind":{"type":"string","description":"A free-text label for filtering the library. Never behavior."},"description":{"type":"string"},"folder_id":{"type":["string","null"]},"template_id":{"type":["string","null"],"description":"Set on a FILLED scene: it has no shots of its own and plays this template’s, with its `fields` filling the blanks. Add shots to the template, not to the filled scene."},"fields":{"type":"object","description":"A filled scene’s values, by blank name."},"ref":{"type":["string","null"],"description":"Your own key for this scene."},"shots":{"type":"array","readOnly":true,"description":"Present on a single-scene read, in playback order.","items":{"type":"object","properties":{"id":{"type":"string"},"position":{"type":"number"},"duration_ms":{"type":"integer"},"layout_id":{"type":["string","null"]}}}},"created_at":{"type":"integer","readOnly":true},"updated_at":{"type":"integer","readOnly":true}},"example":{"id":"scn_welcome","title":"Welcome","kind":"Hero","description":"","folder_id":null,"template_id":null,"fields":{},"ref":null,"shots":[{"id":"sht_a1","position":1,"duration_ms":8000,"layout_id":null}],"created_at":1789372800000,"updated_at":1789372800000}},"BlockKind":{"type":"object","description":"One kind of block a shot can hold. Generated from this deployment’s registry, so it describes the app you are talking to.","properties":{"kind":{"type":"string","description":"What goes in `Block.kind`."},"label":{"type":"string"},"icon":{"type":"string"},"hint":{"type":"string"},"track":{"type":"string","enum":["main","overlay"],"description":"The visual row a new one lands on."},"palette":{"type":"integer","description":"Where it sits in the editor’s palette; absent = not offered there."},"uses_time":{"type":"boolean","description":"Reads the playhead, so the stage keeps time for it."},"compose_only":{"type":"array","items":{"type":"string"},"description":"Fields the server fills when a screen asks. **Never send these** — they are dropped."},"sets":{"type":"array","items":{"type":"string"},"description":"The fields this kind carries beyond the generic five, derived from `template`."},"template":{"$ref":"#/components/schemas/Block"},"rule_presets":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"}}}}},"example":{"kind":"caption","label":"Caption","icon":"text-t","track":"overlay","palette":10,"uses_time":false,"compose_only":[],"sets":["text","font_weight","color","align"],"template":{"uuid":"caption_mu6nc9x01","kind":"caption","track_id":"visual-overlay","timing":{"in_ms":0,"animation":"fade","anim_ms":700},"transform":{"x":384,"y":459,"w":1152,"h":162,"rotate":0,"fit":"cover","scale":1,"opacity":1},"text":"{title}","font_weight":700,"color":"#ffffff","align":"center","rules":[{"id":"r1","label":"Hide once live","enabled":true,"match":"all","when":[{"id":"c1","sensor":"session","property":"status","op":"is","value":{"status":"live"}}],"set":{"hidden":true},"fade_ms":1200}]},"rule_presets":[]}},"SceneCreate":{"type":"object","required":["title"],"properties":{"title":{"type":"string"},"kind":{"type":"string"},"description":{"type":"string"},"folder_id":{"type":["string","null"]},"ref":{"type":["string","null"]}},"example":{"title":"Welcome","kind":"Hero"}},"Condition":{"type":"object","required":["sensor","op"],"description":"One reading, compared. `sensor` and `op` must come from `/named-conditions/sensors`.","properties":{"id":{"type":"string","description":"Any stable string. Generated if you leave it out."},"sensor":{"type":"string","description":"What to read, e.g. `clock`, `weekday`, `session`, `field`, `trait`."},"property":{"type":"string","description":"WHICH reading, for a sensor that has several (the session's `status` vs its `start`). Omit for a sensor with one."},"op":{"type":"string","description":"The comparison, e.g. `after`, `is`, `in`."},"value":{"type":"object","description":"Operator-specific, e.g. `{ \"time\": \"18:00\" }`."}},"example":{"id":"c1","sensor":"clock","op":"after","value":{"time":"18:00"}}},"ConditionGroup":{"type":"object","required":["all","when"],"description":"A bracket. Its children are judged together, then count as one — this is how `A and (B or C)` is expressed. Groups nest up to 3 deep. An empty group is dropped.","properties":{"id":{"type":"string"},"all":{"type":"string","enum":["all","any"],"description":"How the children combine."},"when":{"type":"array","items":{"$ref":"#/components/schemas/ConditionNode"}}},"example":{"id":"g1","all":"any","when":[{"id":"c2","sensor":"weekday","op":"in","value":{"days":[5]}},{"id":"c3","sensor":"weekday","op":"in","value":{"days":[6]}}]}},"ConditionNode":{"description":"Either a single condition or a group of them.","oneOf":[{"$ref":"#/components/schemas/Condition"},{"$ref":"#/components/schemas/ConditionGroup"}],"example":{"id":"c1","sensor":"clock","op":"after","value":{"time":"18:00"}}},"RuleEffect":{"type":"object","description":"What changes WHILE the rule holds. Every field optional; only the ones set apply. Nothing is written to the block — when the rule stops holding, the block is itself again.","properties":{"opacity":{"type":"number","minimum":0,"maximum":1,"description":"Multiplied with the block’s own."},"hidden":{"type":"boolean"},"color":{"type":"string"},"text":{"type":"string","description":"Replaces the text; `{blanks}` fill as the block’s own do."},"scale":{"type":"number","description":"Multiplied with the block’s own."},"filter":{"type":"string","description":"CSS filter, e.g. `grayscale(1)`."},"background":{"type":"string","description":"Canvas rules only."},"backdrop":{"type":"object","description":"Canvas rules only."},"props":{"type":"object","description":"A block kind’s own settings to override."}},"example":{"hidden":true}},"Rule":{"type":"object","description":"“While these conditions hold, this block looks like that.” A rule never edits the block, so nothing has to be undone when it stops holding. Later rules in the list win over earlier ones.","required":["when","set"],"properties":{"id":{"type":"string"},"label":{"type":"string","description":"For the author; never shown on screen."},"enabled":{"type":"boolean","description":"Default true."},"match":{"type":"string","enum":["all","any"],"description":"How the top-level entries of `when` combine."},"when":{"type":"array","items":{"$ref":"#/components/schemas/ConditionNode"},"description":"Empty never holds."},"set":{"$ref":"#/components/schemas/RuleEffect"},"fade_ms":{"type":"integer","description":"How long the change takes, 0–60000. Default 800."},"phase":{"type":"string","enum":["fill"],"description":"Missing = judged on the screen, continuously. `fill` = judged once where the scene is filled with its record, never on the screen: only `field` conditions mean anything and only `set.hidden` applies (the block is left out). A \"Show only when…\" rule — what a block’s `show_when` is stored as. The `field` operators `filled` / `not_filled` ask about the block’s own `{blanks}`."}},"example":{"id":"r1","label":"Hide once live","enabled":true,"match":"all","when":[{"id":"c1","sensor":"session","property":"status","op":"is","value":{"status":"live"}}],"set":{"hidden":true},"fade_ms":1200}},"NamedCondition":{"type":"object","required":["key","label","match","when"],"properties":{"key":{"type":"string","description":"Lowercase letters, digits and underscores. THE ADDRESS: rules store the key, so renaming means moving every reference."},"label":{"type":"string","description":"What a person calls it. Defaults to the key."},"description":{"type":"string","description":"What it means, in one line."},"match":{"type":"string","enum":["all","any"],"description":"How the top-level entries combine. Default `all`."},"when":{"type":"array","items":{"$ref":"#/components/schemas/ConditionNode"},"description":"At least one entry."},"reads_as":{"type":"string","readOnly":true,"description":"The whole thing as a sentence."},"created_at":{"type":"integer","readOnly":true},"updated_at":{"type":"integer","readOnly":true}},"example":{"key":"friday_evening","label":"Friday evening","description":"After six, at the end of the week.","match":"all","when":[{"id":"c1","sensor":"clock","op":"after","value":{"time":"18:00"}},{"id":"g1","all":"any","when":[{"id":"c2","sensor":"weekday","op":"in","value":{"days":[5]}},{"id":"c3","sensor":"weekday","op":"in","value":{"days":[6]}}]}],"reads_as":"the time is from 18:00 and (the weekday is Fri or the weekday is Sat)"}},"SensorReading":{"type":"object","properties":{"sensor":{"type":"string"},"label":{"type":"string"},"readings":{"type":"array","description":"One per reading the sensor offers, each with its own operators and a ready-made example.","items":{"type":"object","properties":{"property":{"type":"string"},"label":{"type":"string"},"ops":{"type":"array","items":{"type":"object","properties":{"op":{"type":"string"},"label":{"type":"string"}}}},"example":{"$ref":"#/components/schemas/Condition"}}}}},"example":{"sensor":"session","label":"The live session","readings":[{"property":"status","label":"where it is","ops":[{"op":"is","label":"is"},{"op":"is_not","label":"is not"}],"example":{"sensor":"session","property":"status","op":"is","value":{"status":"live"}}}]}},"Release":{"type":"object","required":["number","note","scope","valid_from","created_at","created_by"],"properties":{"number":{"type":"integer"},"note":{"type":"string"},"scope":{"oneOf":[{"type":"string","enum":["all"]},{"type":"object","properties":{"stage_ids":{"type":"array","items":{"type":"string"}}}}],"description":"`all`, or only these stages."},"valid_from":{"type":["integer","null"],"description":"A scheduled publish: epoch ms it takes effect."},"rollback_of":{"type":["integer","null"]},"canceled":{"type":"boolean"},"documents":{"type":"integer","description":"How many documents it holds."},"created_at":{"type":"integer"},"created_by":{"type":"string"}},"example":{"number":43,"note":"Lunch menu for Friday","scope":"all","valid_from":null,"rollback_of":null,"canceled":false,"documents":212,"created_at":1790000000000,"created_by":"Ana Muster"}},"PublishResult":{"type":"object","required":["release","changes","stage_ids"],"properties":{"release":{"$ref":"#/components/schemas/Release"},"changes":{"type":"integer","description":"Documents changed since the previous release."},"stage_ids":{"type":"array","items":{"type":"string"},"description":"Stages the changes reach."}},"example":{"release":{"number":43,"note":"Lunch menu","scope":"all","valid_from":null,"created_at":1790000000000,"created_by":"Ana Muster"},"changes":3,"stage_ids":["stg_lobby","stg_bar"]}},"Folder":{"type":"object","required":["id","name"],"description":"Filing and nothing else (ADR-0012): a folder changes nothing about how a file is used, only where it is found. Folders know their parent; they never list their contents.","properties":{"id":{"type":"string"},"name":{"type":"string"},"parent_id":{"type":["string","null"],"description":"The folder this one sits in; `null` at the top level."},"path":{"type":"array","readOnly":true,"description":"Top level first, this folder last — ready to render as a breadcrumb.","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}}},"created_at":{"type":"integer","readOnly":true},"updated_at":{"type":"integer","readOnly":true}},"example":{"id":"fld_sponsors","name":"Sponsors","parent_id":null,"path":[{"id":"fld_sponsors","name":"Sponsors"}],"created_at":1789372800000,"updated_at":1789372800000}},"FolderWrite":{"type":"object","properties":{"name":{"type":"string","maxLength":80},"parent_id":{"type":["string","null"],"description":"Omit to leave it where it is; `null` for the top level."}},"example":{"name":"Sponsors","parent_id":null}},"Asset":{"type":"object","required":["id","kind","name","mime","size","created_at"],"properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["image","video","audio"]},"name":{"type":"string","maxLength":200},"mime":{"type":"string","description":"The file type, e.g. `image/jpeg`."},"size":{"type":"integer","description":"Bytes."},"width":{"type":["integer","null"],"description":"Pixels, when known. Files uploaded through the API are measured when first opened in the app."},"height":{"type":["integer","null"]},"duration_ms":{"type":["integer","null"],"description":"Length of video and audio, when known."},"url":{"type":["string","null"],"description":"A signed download URL, valid for about an hour."},"folder_id":{"type":["string","null"],"description":"The folder it is filed in; `null` at the top level."},"created_at":{"type":"integer","description":"Epoch ms."}},"example":{"id":"Hq3vK0p9aT2xYb7LmN4sW","kind":"image","name":"keynote-backdrop.jpg","mime":"image/jpeg","size":482113,"width":null,"height":null,"duration_ms":null,"url":"https://…signed…","folder_id":null,"created_at":1789372800000}}},"responses":{"E400":{"description":"The request is invalid — `details.field` names the field when there is one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E403":{"description":"The key lacks the scope, or the role is read-only.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E404":{"description":"Not found in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E409":{"description":"Conflicts with the current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E413":{"description":"Too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E415":{"description":"Unsupported media type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E423":{"description":"Closed: the engagement takes no posts right now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E429":{"description":"Too many requests for this key: wait `details.retry_after_s` seconds (also in `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"E503":{"description":"A service this call needs is not available right now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}