{"openapi":"3.1.0","info":{"title":"Airframe API","version":"1.0.0","description":"Publish compositions, link each output to a composition and control its live graphics.\n\nEach output plays one composition, at a version you choose. Things you list and change (versions, the output's composition, instances) are REST resources. Actions on an output are **commands**: send a batch to `POST /v1/outputs/{id}/commands`, e.g. `{ \"commands\": [{ \"command\": \"in\", \"component\": \"…\", \"data\": { \"name\": \"Jane Doe\" } }] }`.\n\nFor live updates, open `GET /v1/outputs/{id}/control/ws?token=…` as a WebSocket. It sends the output's state, patches as things change, and the active version and instances; send it `{ \"type\": \"commands\", \"id\": \"…\", \"commands\": [...] }` and it answers with an ack carrying the same id."},"components":{"securitySchemes":{"session":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Signed in to Airframe. Used by the app."},"apiKey":{"type":"http","scheme":"bearer","description":"A workspace API key (`afk_…`), made in Organisation settings › API keys. It controls every output in the workspace, or the ones chosen for it: commands, state and instances. Older output tokens (`afo_…`) work the same way."}},"schemas":{"Version":{"type":"object","properties":{"id":{"type":"string"},"compositionId":{"type":"string"},"number":{"type":"integer","example":4},"note":{"type":"string","example":"New sponsor logo"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"}},"required":["id","compositionId","number","note","createdBy","createdAt"]},"Error":{"type":"object","properties":{"error":{"type":"string","example":"Not found"},"code":{"type":"string","example":"not_found"}},"required":["error","code"]},"OutputState":{"type":"object","properties":{"revision":{"type":"integer"},"active":{"$ref":"#/components/schemas/ActiveVersion"},"onAir":{"type":"object","additionalProperties":{"type":"boolean"},"description":"Layers, components and elements taken off or put on air, by id"},"data":{"type":"object","properties":{"global":{"$ref":"#/components/schemas/DataValues"},"layers":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/DataValues"}},"components":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/DataValues"}}},"required":["global","layers","components"]},"timers":{"type":"object","additionalProperties":{"type":"object","properties":{"text":{"type":"string"},"running":{"type":"boolean"}},"required":["text","running"]},"description":"Every timer field by id, as it reads now, e.g. { \"f-clock\": { \"text\": \"04:59\", \"running\": true } }"},"liveInstances":{"type":"object","additionalProperties":{"type":"string"}}},"required":["revision","active","onAir","data","timers","liveInstances"]},"ActiveVersion":{"type":["object","null"],"properties":{"compositionId":{"type":"string"},"versionId":{"type":"string"},"versionNumber":{"type":"integer"}},"required":["compositionId","versionId","versionNumber"],"description":"The published version the output plays"},"DataValues":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"description":"Values by field key. Null clears a key, so it goes back to its default.","example":{"name":"Jane Doe","title":"CEO"}},"Revision":{"type":"object","properties":{"revision":{"type":"integer"}},"required":["revision"]},"CommandError":{"type":"object","properties":{"error":{"type":"string","example":"No component nope"},"code":{"type":"string","enum":["command_failed"]},"index":{"type":"integer","description":"Which command in the batch failed"}},"required":["error","code","index"]},"CommandBatch":{"type":"object","properties":{"commands":{"type":"array","items":{"$ref":"#/components/schemas/Command"},"minItems":1,"maxItems":100}},"required":["commands"]},"Command":{"anyOf":[{"type":"object","properties":{"command":{"type":"string","enum":["in"]},"component":{"type":"string","minLength":1,"maxLength":200,"description":"Component id"},"data":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"description":"Merged into the component's data, which keeps it","example":{"name":"Jane Doe","title":"CEO"}},"instance":{"type":"string","minLength":1,"maxLength":200,"description":"An instance of the component, whose data replaces its own"}},"required":["command","component"],"additionalProperties":false,"description":"Shows a component, playing its In. Send data or an instance to show it with. If it's already showing, it just takes the data, as update would."},{"type":"object","properties":{"command":{"type":"string","enum":["out"]},"scope":{"type":"string","enum":["component"]},"component":{"type":"string","minLength":1,"maxLength":200,"description":"Component id"}},"required":["command","component"],"additionalProperties":false,"description":"Takes a component off air, playing its Out"},{"type":"object","properties":{"command":{"type":"string","enum":["out"]},"scope":{"type":"string","enum":["layer"]},"layer":{"type":"string","minLength":1,"maxLength":200,"description":"Layer id"}},"required":["command","scope","layer"],"additionalProperties":false,"description":"Takes every component on a layer off air"},{"type":"object","properties":{"command":{"type":"string","enum":["out"]},"scope":{"type":"string","enum":["root"]}},"required":["command","scope"],"additionalProperties":false,"description":"Takes everything off air"},{"type":"object","properties":{"command":{"type":"string","enum":["update"]},"scope":{"type":"string","enum":["component"]},"component":{"type":"string","minLength":1,"maxLength":200,"description":"Component id"},"data":{"$ref":"#/components/schemas/DataValues"}},"required":["command","component","data"],"additionalProperties":false,"description":"Merges values into a component's data. On air, a change to a trigger field plays its Change."},{"type":"object","properties":{"command":{"type":"string","enum":["update"]},"scope":{"type":"string","enum":["layer"]},"layer":{"type":"string","minLength":1,"maxLength":200,"description":"Layer id"},"data":{"$ref":"#/components/schemas/DataValues"}},"required":["command","scope","layer","data"],"additionalProperties":false,"description":"Merges values into a layer's data"},{"type":"object","properties":{"command":{"type":"string","enum":["update"]},"scope":{"type":"string","enum":["root"]},"data":{"$ref":"#/components/schemas/DataValues"}},"required":["command","scope","data"],"additionalProperties":false,"description":"Merges values into the composition's global data"},{"type":"object","properties":{"command":{"type":"string","enum":["invoke"]},"component":{"type":"string","minLength":1,"maxLength":200,"description":"Component id"},"event":{"type":"string","minLength":1,"maxLength":200,"description":"Event id"}},"required":["command","component","event"],"additionalProperties":false,"description":"Plays one of a component's events"},{"type":"object","properties":{"command":{"type":"string","enum":["timer"]},"timer":{"type":"string","minLength":1,"maxLength":200,"description":"Timer field id"},"action":{"type":"string","enum":["start","pause","reset","set"]},"ms":{"type":"number","minimum":0,"description":"For set: milliseconds"},"time":{"type":"string","maxLength":20,"description":"For set: e.g. \"5:00\""}},"required":["command","timer","action"],"additionalProperties":false,"description":"Starts, pauses, resets or sets a countdown or count-up"}]},"OutputComposition":{"type":"object","properties":{"composition":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"description":"The composition the output is linked to"},"active":{"$ref":"#/components/schemas/ActiveVersion"},"latestVersion":{"type":["integer","null"],"description":"The composition's newest version, to offer an update when it's newer"}},"required":["composition","active","latestVersion"]},"Instance":{"type":"object","properties":{"id":{"type":"string"},"componentId":{"type":"string"},"name":{"type":"string","example":"Jane Doe, CEO"},"data":{"$ref":"#/components/schemas/DataValues"},"isDefault":{"type":"boolean","description":"Every component has one default instance (id `default:<componentId>`). It can be edited and taken, but not removed."}},"required":["id","componentId","name","data"]}},"parameters":{}},"paths":{"/compositions/{id}/versions":{"get":{"tags":["Versions"],"summary":"List a composition's published versions, newest first","security":[{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The versions","content":{"application/json":{"schema":{"type":"object","properties":{"versions":{"type":"array","items":{"$ref":"#/components/schemas/Version"}}},"required":["versions"]}}}},"404":{"description":"No such composition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Versions"],"summary":"Publish the composition as it is now as a new, immutable version","security":[{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"note":{"type":"string","maxLength":200}}}}}},"responses":{"201":{"description":"Published","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Version"}}}},"403":{"description":"Operators can't publish","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such composition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/compositions/{id}/versions/{number}":{"get":{"tags":["Versions"],"summary":"One version, with its document","security":[{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"integer","exclusiveMinimum":0,"example":4},"required":true,"name":"number","in":"path"}],"responses":{"200":{"description":"The version and its composition document","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Version"},{"type":"object","properties":{"document":{"type":"object"}},"required":["document"]}]}}}},"404":{"description":"No such composition or version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/outputs/{id}/state":{"get":{"tags":["Control"],"summary":"What's on the output now","security":[{"apiKey":[]},{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutputState"}}}},"401":{"description":"No session or API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such output","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/outputs/{id}/commands":{"post":{"tags":["Control"],"summary":"Run a batch of commands, in order, all or nothing","description":"Each command names what it does: `in`, `out`, `update`, `invoke` or `timer`. Components, layers, events, timers and instances are named by id; data by field key. `out` and `update` act on a component by default, or on a whole layer or everything with `scope: \"layer\"` or `\"root\"`.\n\nEvery command in the batch runs under one revision. If any can't run, none do, and the response says which. The control WebSocket takes the same commands.","security":[{"apiKey":[]},{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandBatch"}}}},"responses":{"200":{"description":"Done","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Revision"}}}},"401":{"description":"No session or API key, or a bad key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such output","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"A command couldn't run, so none did","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandError"}}}},"429":{"description":"Too many commands; wait and retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/outputs/{id}/composition":{"get":{"tags":["Composition"],"summary":"The composition the output plays, and which version","security":[{"apiKey":[]},{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The output's composition and active version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutputComposition"}}}},"404":{"description":"No such output","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Composition"],"summary":"Link the output to a composition, or move it to another version","description":"Loads the version on the output, the newest by default. Another version of the same composition keeps what's on air, its data and instances; a different composition starts with nothing on air and no instances.","security":[{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"compositionId":{"type":"string"},"versionNumber":{"type":"integer","exclusiveMinimum":0,"description":"Defaults to the newest version"}},"required":["compositionId"]}}}},"responses":{"200":{"description":"The output's composition and active version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutputComposition"}}}},"403":{"description":"Operators and API keys can't change the composition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such output, composition or version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/outputs/{id}/instances":{"get":{"tags":["Instances"],"summary":"The instances prepared for the output's composition","security":[{"apiKey":[]},{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The instances","content":{"application/json":{"schema":{"type":"object","properties":{"instances":{"type":"array","items":{"$ref":"#/components/schemas/Instance"}},"liveInstances":{"type":"object","additionalProperties":{"type":"string"},"description":"Which instance each live component is showing, by component id"}},"required":["instances","liveInstances"]}}}}}},"post":{"tags":["Instances"],"summary":"Prepare an instance of a component","security":[{"apiKey":[]},{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"componentId":{"type":"string","minLength":1,"maxLength":200},"name":{"type":"string","minLength":1,"maxLength":120},"data":{"$ref":"#/components/schemas/DataValues"}},"required":["componentId","name"]}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Instance"}}}},"404":{"description":"The active version has no such component","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/outputs/{id}/instances/{instance}":{"patch":{"tags":["Instances"],"summary":"Rename an instance or change its data","description":"`data` merges in; null clears a key. Changing the instance that's on air updates the graphic straight away.","security":[{"apiKey":[]},{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"instance","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"data":{"$ref":"#/components/schemas/DataValues"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Instance"}}}},"404":{"description":"No such instance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Instances"],"summary":"Remove an instance","security":[{"apiKey":[]},{"session":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"instance","in":"path"}],"responses":{"204":{"description":"Removed"},"404":{"description":"No such instance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Default instances can't be removed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}