# impartire API — full documentation > The impartire developer API: assets and files, workflow runs, delivery and EPG reads, guarded actions a person confirms, and agents over MCP. This file is the plain-text form of https://impartire.com/docs. ## Contents Each section is also its own Markdown file, small enough to fetch whole. The reference index links one file per resource group. - [Quickstart](https://impartire.com/docs/quickstart.md): Upload a file and create an asset in five requests; the common tasks after that. - [Authentication](https://impartire.com/docs/authentication.md): API keys and scopes, OAuth, connecting Claude Code, Claude and ChatGPT over MCP, the discovery documents. - [Conventions](https://impartire.com/docs/conventions.md): Problem Details errors, Idempotency-Key, ETag and If-Match, pagination, rate limits, Request-Id, versioning. - [MCP tools](https://impartire.com/docs/mcp.md): The MCP server URL and every tool with its scopes. - [Problems](https://impartire.com/docs/problems.md): Every error `type`: its status, what it means, what to do, and an example body. - [Reference index](https://impartire.com/docs/reference.md): Every operation, one line each, grouped, with a link to each group's file. ## Quickstart Upload a file and create an asset from it with five requests. You need an org whose admin can mint API keys. ### 1. Mint a key In impartire, open Settings → API keys, name the key, tick `assets:write` and `projects:read`, and create it. The key (`impk_…`) is shown once; keep it in an environment variable. ```bash export IMPARTIRE_KEY=impk_… ``` ### 2. Check who you are ```bash curl -s https://impartire.com/api/v1/me -H "Authorization: Bearer $IMPARTIRE_KEY" ``` The answer names the org, the key, its scopes and its request budget. ### 3. Find a project and an asset type ```bash curl -s "https://impartire.com/api/v1/projects?limit=20" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s https://impartire.com/api/v1/asset-types -H "Authorization: Bearer $IMPARTIRE_KEY" ``` Take a project's `id` and an asset type's `id`. A type lists its file slots and the extensions each accepts. ### 4. Upload the bytes ```bash curl -s -X POST https://impartire.com/api/v1/uploads -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"filename":"promo.mp4","content_type":"video/mp4","size_bytes":'"$(wc -c < promo.mp4)"'}' # then PUT the file to upload_url, with every header in the answer's `headers` curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @promo.mp4 ``` ### 5. Create the asset, then read it back ```bash curl -s -X POST https://impartire.com/api/v1/assets -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"type_id":"","project_id":"","name":"Promo","file":{"upload_id":""}}' curl -s https://impartire.com/api/v1/assets/ -H "Authorization: Bearer $IMPARTIRE_KEY" ``` The file is attached to the type's slot and its pipeline (proxy, captions, QC) starts as it would for a browser upload; `runs` on the asset shows what it set off, and `files` gains the derived files as they land. ### Then: start a workflow run and wait for it A key with `workflows:read` and `runs:write` can start any of the org's workflow templates on an asset. List the templates; each `param_schema` names the launch parameters `params` must carry (a missing required one, or one the template does not declare, is a 422 naming it). ```bash curl -s https://impartire.com/api/v1/workflows -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s -X POST https://impartire.com/api/v1/runs -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"workflow_id":"","asset_id":"","params":{}}' ``` The answer is 202 with the run and a `Location`: it has started, not finished. Poll it every few seconds until `status` leaves `active` — or until `current_step.awaiting` is `approval` or `completion`, which means the run waits for a person in the app. While the pipeline works, `awaiting` is `automation`. `incident` is set when a step failed and waits for an operator. ```bash curl -s https://impartire.com/api/v1/runs/ -H "Authorization: Bearer $IMPARTIRE_KEY" # or every run that changed since you last looked curl -s "https://impartire.com/api/v1/runs?updated_after=2026-10-20T12:00:00Z" -H "Authorization: Bearer $IMPARTIRE_KEY" ``` A template that delivers content out of the org — itself, through a step, or through a sub-workflow — is never started through `POST /runs`, with any credential: it answers 403 [`confirmation-required`](https://impartire.com/problems/confirmation-required). Deliver against a requirement instead (below). A run waiting on a person is signed off with `POST /runs/{id}/signals` (`runs:signal`; a key may only cancel). ### Then: tag it With `assets:write`, put tags on an asset by name: a name the org already has is that tag (case and spacing ignored), a new one becomes an org tag. Adding a tag the asset carries changes nothing, so the call is safe to repeat. `tags:read` lists the org's tags; `GET /assets?tag=` finds the assets carrying one. ```bash curl -s -X POST https://impartire.com/api/v1/assets//tags -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -d '{"tags":[{"name":"Interview"},{"name":"B-roll"}]}' curl -s "https://impartire.com/api/v1/tags?q=inter" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s "https://impartire.com/api/v1/assets?tag=interview" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s -X DELETE https://impartire.com/api/v1/assets//tags/ -H "Authorization: Bearer $IMPARTIRE_KEY" ``` A team's private tag is added by `{ "id" }` only; projects take tags the same way at `/projects/{id}/tags` with `projects:write`. ### Then: make the project it goes in With `projects:write`, create a project to file assets into — a root (a production, born Closed: the org sees its name, admitted teams open it) or a sub-project under `parent_id`. A typed project follows the org's hierarchy rules (422 [`invalid-hierarchy`](https://impartire.com/problems/invalid-hierarchy) names the rule). The key is the new project's owner. Rename, describe or retype it with `PATCH` under the ETag the create returned. ```bash curl -si -X POST https://impartire.com/api/v1/projects -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"name":"Season 2","parent_id":""}' # -i: the ETag header comes with the body curl -s -X PATCH https://impartire.com/api/v1/projects/ -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H 'If-Match: ""' -d '{"description":"Autumn run"}' ``` ### Then: point a record at it If the org keeps custom records (a Product, a Contract, a Talent release), `GET /record-types` lists their types and fields, with `records:read`. With `records:write`, create one whose reference field holds the asset's id; `GET /assets/{id}/referenced-by` then lists it (with `assets:read`). A record's `PATCH` replaces its whole `fields` under `If-Match`, as an asset's does. ```bash curl -s https://impartire.com/api/v1/record-types -H "Authorization: Bearer $IMPARTIRE_KEY" curl -si -X POST https://impartire.com/api/v1/records -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"type_id":"","title":"Harbour Line — distribution","fields":{"":""}}' curl -s https://impartire.com/api/v1/assets//referenced-by -H "Authorization: Bearer $IMPARTIRE_KEY" ``` ### Then: read its review and its lineage With `review:read`, `GET /assets/{id}/comments` lists the asset's review comments — flat, threaded by `parent_id`, each pinned to `timestamp_seconds` in the media or to the whole asset — and `resolved=false` keeps the open ones; `GET /assets/{id}/markers` lists its clip markers (at most three, returned whole). With `review:write`, `POST` a comment (or a reply, `parent_id`) or a marker, `PATCH` a comment to resolve or reopen it (or, as its author, change its text), and `DELETE` either; a key writes as the org's API, shown as `API` in the app. With `assets:read`, `GET /assets/{id}/lineage` shows what the asset derives from and what derives from it (proxies, promoted clips, deliveries). Where the graph leaves your organisation — a delivery to or from another org — the node is a sealed `boundary` naming the other org and the receipt, never its asset. ```bash curl -s "https://impartire.com/api/v1/assets//comments?resolved=false" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s https://impartire.com/api/v1/assets//markers -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s https://impartire.com/api/v1/assets//lineage -H "Authorization: Bearer $IMPARTIRE_KEY" ``` ### Then: read who is credited on it and how it is categorised With `assets:read`, `GET /assets/{id}/credits` lists the people credited on the asset (role and billing order) and `GET /assets/{id}/categories` the categories assigned to it. With `people:read`, `GET /people` lists the org's people (search with `q`, poll with `updated_after`) and `GET /people/{id}/credits` walks a person's credits back to the assets. With `taxonomy:read`, `GET /categories` returns the org's whole asset taxonomy, each category with its group; filter assets by one with `GET /assets?category_id=`. All read only: credits, people and categories are managed in the app. ```bash curl -s https://impartire.com/api/v1/assets//credits -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s "https://impartire.com/api/v1/people?q=okafor" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s https://impartire.com/api/v1/people//credits -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s https://impartire.com/api/v1/categories -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s https://impartire.com/api/v1/assets//categories -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s "https://impartire.com/api/v1/assets?category_id=" -H "Authorization: Bearer $IMPARTIRE_KEY" ``` ### Then: play it `GET /assets/{id}/playback` is what a player needs for the asset's video: `status`, the HLS manifest in `hls_url` (it carries its own access token when the delivery is signed — hand it to any HLS player as is), a `poster_url`, the WebVTT `captions` (the first is the default track) and progressive MP4 `proxies`, tallest first. `status` is `ready` when the HLS ladder plays, `processing` while it is being made (nothing else is set — ask again in a minute), and `none` when there is no ladder: no video, a transcode that failed or never ran, or a delivery the API cannot play yet. Every URL is short-lived: `expires_at` is the earliest moment one may stop working, so **fetch the descriptor again rather than caching a URL**. The original upload is never in it — playing is a read, the original is an export (`…/files/{file_id}/download`, which an OAuth token's person needs export permission for). It needs `assets:read`. ```bash curl -s https://impartire.com/api/v1/assets//playback -H "Authorization: Bearer $IMPARTIRE_KEY" # → { "status": "ready", "hls_url": "https://…/manifest.m3u8?token=…", "expires_at": "…", "captions": [ … ], "proxies": [ … ] } ``` ### Then: see who changed it With `audit:read`, `GET /audit-log` is the org's audit trail, newest first: each entry says what was created, updated or deleted, who did it (a person, an API key, an app acting for a person, a workflow run, the system) and which fields changed — a projected feed, never the stored row images, and only the keys of `fields`. `resource_id` narrows it to one resource (for an asset, a version's id), `request_id` to everything one request did, and `resource_type`, `actor_kind`, `action`, `since` and `until` filter further. An `event` entry records what a change meant (an asset created through the API, a legal hold placed); its changes are on the entry with the same `request_id`. An OAuth token's person needs permission to view the audit log. ```bash curl -s "https://impartire.com/api/v1/audit-log?resource_id=" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s "https://impartire.com/api/v1/audit-log?resource_type=collection&action=deleted&since=2026-10-01T00:00:00Z" -H "Authorization: Bearer $IMPARTIRE_KEY" ``` ### Then: see whether it is ready to deliver, and what was delivered With `delivery:read`, `GET /delivery-requirements` lists the org's standing demands — a project's subtree must meet a pinned spec profile, for a client, delivered to a target — and `GET /delivery-requirements/{id}/readiness` judges every asset in that subtree against the spec, a page of assets at a time: `passed`, `failed` (with the failures and whether a conform can fix them), `indeterminate` or `pending`. Reading readiness evaluates a file that has no verdict yet, as the app's matrix does. `GET /delivery-receipts` is the ledger of every delivery attempt, newest first (filter by `asset_id`, `requirement_id`, `status`; poll with `created_after`); `GET /spec-profiles` the specs your org may read, its own and those another org published to it; `GET /delivery-targets` where it delivers, never with credentials. An org that receives deliveries through a supply link reads them at `GET /inbound-deliveries`. All read only; an OAuth token's person needs the read-delivery permission. ```bash curl -s "https://impartire.com/api/v1/delivery-requirements?status=active" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s https://impartire.com/api/v1/delivery-requirements//readiness -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s "https://impartire.com/api/v1/delivery-receipts?asset_id=" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s https://impartire.com/api/v1/spec-profiles/ -H "Authorization: Bearer $IMPARTIRE_KEY" ``` ### Then: deliver it With `delivery:write` (a guarded scope — see Confirming an action), `POST /delivery-requirements/{id}/deliver` sends an asset's file to the requirement's destination, or to `target_id`. Only a file that passes the requirement's spec goes — the best one, a conformed rendition first, unless `file_id` names one (readiness shows which pass). A key's delivery starts at once: 202 with the Deliver `run` and the `receipt`, `pending` until the push lands; poll either. The same file already on its way to the same destination for the requirement is 409 [`delivery-in-flight`](https://impartire.com/problems/delivery-in-flight), naming that receipt; send an `Idempotency-Key` to retry a request safely. An asset under legal hold is 409 [`legal-hold`](https://impartire.com/problems/legal-hold). With an OAuth token the delivery waits for its person to confirm it. An org that receives a delivery through a supply link can refuse it: `POST /delivery-receipts/{id}/reject` with a `rejection_note` (the id from `GET /inbound-deliveries`). The supplier reads the note on their receipt; the landed asset stays in your intake project. A token's person must be an org admin, and confirms it in impartire. ```bash curl -s -X POST https://impartire.com/api/v1/delivery-requirements//deliver -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{ "asset_id": "" }' # the receiving org, refusing what landed curl -s -X POST https://impartire.com/api/v1/delivery-receipts//reject -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -d '{ "rejection_note": "Audio is stereo; the spec needs 5.1." }' ``` ### Then: see what airs on your channels With `epg:read`, `GET /services` lists the org's linear services (EPG channels) — each with its transmission spec and the clock formats bound to it — and `GET /schedules?service_id=&from=&to=` the programmes on one service that overlap a window of up to 31 days (`from` and `to` are ISO 8601 with an offset), by start time, each with the asset and the pinned file that airs and its `readiness` against the service's spec as last evaluated — reading never re-evaluates it. `GET /transmission-log` is the same service's planned log for up to 7 days: each programme, and the breaks and IDs its format places inside it. It is derived from the schedule, so it changes whenever the schedule does; it is not an as-run record. `GET /formats` lists the clocks themselves, each element with its duration in milliseconds. Poll services, slots and formats with `updated_after`; a deleted slot simply stops appearing, so re-read the window. ```bash curl -s https://impartire.com/api/v1/services -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s "https://impartire.com/api/v1/schedules?service_id=&from=2026-10-18T00:00:00Z&to=2026-10-19T00:00:00Z" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s "https://impartire.com/api/v1/transmission-log?service_id=&from=2026-10-18T00:00:00Z&to=2026-10-19T00:00:00Z" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s https://impartire.com/api/v1/formats/ -H "Authorization: Bearer $IMPARTIRE_KEY" ``` ### Then: change what airs With `schedules:write` (a guarded scope — see Confirming an action), `POST /schedules` puts a programme on a service: `service_id`, `title`, `starts_at` and `ends_at`, and what airs — an `asset_id` (its current version's id or its root id; the file that airs is pinned then, `file_id` or the asset's primary video), or a rotation source (`source_project_id` or `source_collection_id`) — with an optional `format_id` and `broadcast_day` (the day a programme after midnight belongs to). Every id must be the org's: one that is not is 422, naming the field. `PATCH /schedules/{id}` moves or changes a slot — send only what changes, with the slot's ETag in `If-Match` (428 without it, 412 when someone changed the slot since you read it) — and `DELETE /schedules/{id}` removes it. Each write re-evaluates the slot's readiness against the service's spec when what airs changes, re-derives the transmission log, and re-pushes the channel's on-air graphic, as the app's grid does. Scheduling an asset under legal hold, or changing which of its files airs, is 409 [`legal-hold`](https://impartire.com/problems/legal-hold); removing a slot never is. A pinned `file_id` must be a file of the scheduled asset (any of its versions). A key's write happens at once (201, 200, 204; send an `Idempotency-Key` with a create to retry it safely). With an OAuth token each write waits for its person — an org admin — to confirm it. ```bash curl -s -X POST https://impartire.com/api/v1/schedules -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{ "service_id": "", "title": "Evening News", "starts_at": "2026-10-30T18:00:00Z", "ends_at": "2026-10-30T18:30:00Z", "asset_id": "" }' # move it half an hour later, under the ETag the create answered with curl -s -X PATCH https://impartire.com/api/v1/schedules/ -H "Authorization: Bearer $IMPARTIRE_KEY" -H 'If-Match: ""' \ -H "Content-Type: application/json" -d '{ "starts_at": "2026-10-30T18:30:00Z", "ends_at": "2026-10-30T19:00:00Z" }' ``` ### Then: read the dashboards, the members and the playout channels With `analytics:read`, `GET /analytics/summary?range=30d` returns the numbers of the app's analytics dashboard — views, unique viewers, playback time and downloads over the range, the same over the range before it, one point per day, the ten most viewed assets and the device and audience breakdowns. A range is counted in the org's reporting days (its time zone and the hour its day starts), so `range.from` and `range.to` are dates, not instants. `viewer_type=api` narrows it to downloads made through this API. `GET /analytics/operations` is how the DAM was operated (runs, approvals, open incidents, deliveries, assets nobody opened) and `GET /analytics/search` what people searched for and did not find. Channel audience is not served yet. With `org:read`, `GET /members` lists the org's members with their email, roles and teams (`q` searches name and email). With `channels:read`, `GET /channels` lists the playout channels as the DAM stores them: mode, the service each airs, whether it records and where. Never a stream key, and never the live on-air state — reading a channel does not ask the playout engine anything. ```bash curl -s "https://impartire.com/api/v1/analytics/summary?range=30d" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s "https://impartire.com/api/v1/analytics/operations?range=7d" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s "https://impartire.com/api/v1/members?q=vega" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s https://impartire.com/api/v1/channels -H "Authorization: Bearer $IMPARTIRE_KEY" ``` ### Then: start or stop a channel With `channels:control` (a guarded scope), `POST /channels/{id}/start` puts a channel's service on air and `POST /channels/{id}/stop` takes it off — the app's own start and stop, through the playout engine: a start pushes the channel's stored configuration and then its on-air graphic. Only a `strict` channel starts or stops this way (a relay channel is 409). The answer is the `Channel` as the DAM stores it, not the engine's live state; the engine's state after the call is recorded in the audit log (`channel.started`, `channel.stopped`). If the engine cannot be reached the answer is 502 [`playout-unavailable`](https://impartire.com/problems/playout-unavailable) and nothing changed on air. A key acts at once; with an OAuth token its person, an org admin, confirms it in impartire. ```bash curl -s -X POST https://impartire.com/api/v1/channels//start -H "Authorization: Bearer $IMPARTIRE_KEY" -H "Idempotency-Key: $(uuidgen)" ``` ### Then: control who can see a project With `access:write` (a guarded scope), `POST /projects/{id}/access` gives a member, a team or a role a level on a project — `viewer` (read it and everything filed under it), `contributor` (edit it too), `manager` (administer it, including who has access), or `asset_contributor` / `asset_purger` (edit or delete the assets under it without reading the project) — and `DELETE /projects/{id}/access?subject_type=&subject_id=&level=` takes one grant away. `subject_type` is `member` (a principal id from `GET /members`), `team` or `role`; a team's or role's grant follows its membership. A subject outside the org is 422; an archived team, or a subject denied on the project, is 409. `POST /projects/{id}/deny` with a `reason` denies a subject everything on the project and below it, whatever else grants it — admins included, never yourself — and `DELETE /projects/{id}/deny?subject_type=&subject_id=` lifts it. `POST /projects/{id}/teams` admits a team to a production: its members read the production and get a private lane on it. `POST /projects/{id}/move` with `parent_id` (or `null` for the top level) moves a project; it then reads through its new root, so a move may narrow who can see it but never widen it (422), and a narrowing revokes the public share links to the assets under it. A key acts at once; with an OAuth token each waits for its person — who must manage the project (administer it, for a team; update it and the new parent, for a move) — to confirm it. Every change is in the audit log (`project.access_granted`, `project.access_denied`, `project.moved`, …). ```bash curl -s -X POST https://impartire.com/api/v1/projects//access -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{ "subject_type": "member", "subject_id": "", "level": "viewer" }' curl -s -X DELETE "https://impartire.com/api/v1/projects//access?subject_type=member&subject_id=&level=viewer" -H "Authorization: Bearer $IMPARTIRE_KEY" ``` ### Then: share an asset by link With `sharing:write` (a guarded scope), `POST /assets/{id}/share-links` makes a public link: anyone holding it can watch the asset without signing in. Optional: `expires_in_days` (1–365; omitted, it never expires), a `password` the viewer types on the link's page (stored salted and hashed, never readable again), `allow_download`, and a clip (`clip_start_seconds`, `clip_end_seconds`). **The answer's `url` is the only time you see it** — the link's token is its whole authority, so `GET /assets/{id}/share-links` (`assets:read`) lists every link's settings, views and creator but never its URL; keep it as you would a password. `DELETE /assets/{id}/share-links/{link_id}` revokes one for everyone at once. An asset under legal hold cannot be shared (409 [`legal-hold`](https://impartire.com/problems/legal-hold)), and placing a hold revokes its links. A key's link is made at once; with an OAuth token its person — who must be able to share the asset — confirms it, and the confirmation's `result.url` is the link. ```bash curl -s -X POST https://impartire.com/api/v1/assets//share-links -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{ "expires_in_days": 7, "password": "", "allow_download": false }' # → { …, "url": "https://impartire.com/share/…" } — shown once ``` ### Then: read its files, their measurements and their versions `GET /assets/{id}/files` lists the asset's current files — the ones attached to a slot, then the ones the pipeline derived — each with `technical`: what the pipeline measured (a video's duration, dimensions, codec, frame rate and loudness; an audio stem's channels and loudness; an image's size and colour space). `technical` is null until the measure pass has run, so poll the file rather than assuming it is there; a derived file's `size_bytes` may be null. A file replaced (through the API or in the app) is a new version in the same chain (`root_file_id`). `GET /assets/{id}/files/{file_id}` takes any version's id and lists the whole chain in `versions`, newest first, with who added each and why; download any version, current or not, with its id. ```bash curl -s https://impartire.com/api/v1/assets//files -H "Authorization: Bearer $IMPARTIRE_KEY" curl -si https://impartire.com/api/v1/assets//files/ -H "Authorization: Bearer $IMPARTIRE_KEY" # -i: the ETag; send it back in If-None-Match for a 304 curl -s https://impartire.com/api/v1/assets//files//download -H "Authorization: Bearer $IMPARTIRE_KEY" ``` ### Then: attach, replace or restore a file An asset that exists takes more files the way it took its first: `POST /uploads`, PUT the bytes, then `POST /assets/{id}/files` with the `upload_id`. Pass `replace_file_id` (a current file's id) to make the upload the next version of that file — the earlier version is kept and stays downloadable; without it, a slot that holds one file gets a new version of it and a slot that holds several gets a new file. The slot's pipeline starts as it did for the first file. An asset under legal hold refuses attach, replace and restore with 409 [`legal-hold`](https://impartire.com/problems/legal-hold). To bring an earlier version back, restore it: the chain gets a new version carrying that version's bytes, nothing is rewritten, and no pipeline runs (the bytes were processed when they first arrived). Restoring the current version is 422 [`already-current`](https://impartire.com/problems/already-current). ```bash curl -si -X POST https://impartire.com/api/v1/assets//files -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"upload_id":"","replace_file_id":"","version_note":"Audio fix, QC 4412"}' curl -s -X POST https://impartire.com/api/v1/assets//files//restore -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` ### Large files: upload in parts One PUT carries at most 5 GiB. Above that, send `multipart: true` with `POST /uploads` (allowed from 5 MiB, up to 5 TiB; without it a file over 5 GiB is a 422 naming `multipart`). The answer has no `upload_url`; its `multipart` says how the file is cut: every part is `part_size_bytes` long (chosen by the server, at least 8 MiB) except the last, and there are `part_count` of them, numbered from 1. Ask `POST /uploads/{id}/parts` for the URLs of up to 100 parts at a time, PUT each part's bytes to its URL (each URL works for 15 minutes; asking again signs a fresh one), and keep the `ETag` header each PUT answers. Then `POST /uploads/{id}/complete` with every part's number and ETag. The answer is the upload's status, `landed`; create the asset or attach the file with the `upload_id` as for a single PUT. Completing again answers the same status. If the parts do not add up to `size_bytes`, the file is discarded with 422 [`size-mismatch`](https://impartire.com/problems/size-mismatch). To give up, `POST /uploads/{id}/abort` frees the stored parts. The MCP tool `impartire_start_upload` does single PUTs only. ```bash curl -s -X POST https://impartire.com/api/v1/uploads -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"filename":"master.mov","content_type":"video/quicktime","size_bytes":'"$(wc -c < master.mov)"',"multipart":true}' # → multipart: { part_size_bytes, part_count, parts_url } curl -s -X POST https://impartire.com/api/v1/uploads//parts -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -d '{"part_numbers":[1,2,3]}' # PUT part n: bytes (n-1)*part_size_bytes onward, part_size_bytes long (the last part: the rest) dd if=master.mov bs=$PART_SIZE skip=$((N-1)) count=1 iflag=skip_bytes,count_bytes 2>/dev/null \ | curl -s -D - -o /dev/null -X PUT "$PART_URL" --data-binary @- | grep -i etag curl -s -X POST https://impartire.com/api/v1/uploads//complete -H "Authorization: Bearer $IMPARTIRE_KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"parts":[{"part_number":1,"etag":""},{"part_number":2,"etag":""},{"part_number":3,"etag":""}]}' ``` ## Authentication Every request except [the OpenAPI document](https://impartire.com/api/v1/openapi.json) carries a credential in the `Authorization` header — never the query string. There are two kinds, and both reach the same endpoints. ### API keys — for your own server An org admin mints a key on Settings → API keys: `Authorization: Bearer impk_…`. A key acts for the org; its scopes are its whole authority, across every project. `:write` includes `:read`. Revoking a key stops it on the next request. | Scope | Allows | | --- | --- | | `assets:read` | Read asset types, assets and their files | | `assets:write` | Upload files, create, edit and delete assets (includes read) | | `projects:read` | List projects and their playlists | | `external-sources:write` | Register items from an external platform (Zype) | | `projects:write` | Create and edit projects; create, edit and delete their playlists (includes read) | | `tags:read` | List the org's tags | | `workflows:read` | List the workflows a run can be started from | | `runs:read` | Read workflow runs and their progress | | `runs:write` | Start workflow runs (includes read) | | `collections:read` | Read collections and their items | | `collections:write` | Create, edit and delete collections and their items (includes read) | | `records:read` | Read record types and records | | `records:write` | Create, edit and delete records (includes read) | | `review:read` | Read assets' review comments and clip markers | | `people:read` | Read the org's people and their credits | | `taxonomy:read` | Read the org's category taxonomy | | `audit:read` | Read the org's audit log: who changed what, and when | | `delivery:read` | Read delivery requirements, readiness, receipts, spec profiles and delivery targets | | `epg:read` | Read the EPG: services, schedules, formats and the transmission log | | `analytics:read` | Read consumption, operations and search analytics | | `org:read` | Read the org's members, with their email addresses, roles and teams | | `channels:read` | Read playout channels and their configuration (no stream keys) | | `review:write` | Add, resolve and delete review comments and clip markers (includes read) | | `runs:signal` | Cancel workflow runs (a key never approves, rejects, completes or skips a step) | | `delivery:write` | Deliver content against a delivery requirement and reject received deliveries (includes read) | | `schedules:write` | Create, change and remove programmes on the EPG schedule | | `channels:control` | Start and stop playout channels | | `access:write` | Grant and deny project access, bind teams to projects and move projects | | `sharing:write` | Create and revoke public share links to assets | ### OAuth — for an app acting for a person Claude, ChatGPT and other MCP clients connect with OAuth 2.1 (authorization code with PKCE). The person signs in to impartire, chooses the workspace the app will work in and allows it; the app then holds a one-hour access token (a JWT) and, with `offline_access`, a refresh token that rotates on every use. A token acts as its person: it sees only what they can see and does only what they may do. They can disconnect the app on Settings → Connected apps; its access token keeps working until it expires, within the hour. A client finds the authorization server from a 401's `WWW-Authenticate: Bearer resource_metadata="…"`, or directly: - [`/.well-known/oauth-protected-resource/mcp`](https://impartire.com/.well-known/oauth-protected-resource/mcp) and [`/.well-known/oauth-protected-resource/api/v1`](https://impartire.com/.well-known/oauth-protected-resource/api/v1) — the two resources (RFC 9728) - [`/.well-known/oauth-authorization-server`](https://impartire.com/.well-known/oauth-authorization-server) — the authorization server (RFC 8414) - Clients identify themselves with a Client ID Metadata Document URL; dynamic client registration is accepted for public clients only. ### Guarded actions Six scopes are guarded: `runs:signal`, `delivery:write`, `schedules:write`, `channels:control`, `access:write` and `sharing:write`. They name actions that attest to work, move content out of the org, change what airs, or change who can see what. A credential that holds one is in the guarded class: `GET /api/v1/me` says `credential_class: "guarded"` (otherwise `"standard"`). `review:write` (comments and markers) is not guarded. - An API key with a guarded scope must expire within 90 days: minting one with no `expires_at`, or a later one, is refused. A key acts at once — the admin who minted it made the decision — but never attests to a person's work: with `runs:signal` it may only cancel a run. - An OAuth token's guarded action never runs when it is asked for: the endpoint answers `202 Accepted` with a `Confirmation`, and the action runs only after its person confirms it in impartire (see Confirming an action, below). - Every guarded action is checked again when it runs — permission, a legal hold, a newer version, a change since you read it — and is recorded in the audit log, naming the key, or the person and the app. | Scope | Guarded endpoints | With a key | | --- | --- | --- | | `runs:signal` | `POST /runs/{id}/signals` | `cancel` only; approve, reject, complete and skip need a person | | `delivery:write` | `POST /delivery-requirements/{id}/deliver`, `POST /delivery-receipts/{id}/reject` | At once | | `schedules:write` | `POST /schedules`, `PATCH /schedules/{id}`, `DELETE /schedules/{id}` | At once | | `channels:control` | `POST /channels/{id}/start`, `POST /channels/{id}/stop` | At once | | `access:write` | `POST` and `DELETE /projects/{id}/access`, `POST` and `DELETE /projects/{id}/deny`, `POST /projects/{id}/teams`, `POST /projects/{id}/move` | At once | | `sharing:write` | `POST /assets/{id}/share-links`, `DELETE /assets/{id}/share-links/{link_id}` | At once | Over MCP the guarded tools are `impartire_signal_run` (`runs:signal`) and `impartire_deliver` (`delivery:write`); the other guarded actions are REST only. ### Confirming an action An OAuth token's guarded action does not run when it is asked for. The endpoint (any in the table above) answers `202 Accepted` with a `Confirmation` and a `Location` — what will be done, on which resource, a `confirm_url`, and an `expires_at` 15 minutes away. Show the person the `confirm_url`: they confirm or decline it in impartire, signed in to that workspace (and with two-factor, if the workspace requires it). On confirm the action runs as them. Poll `GET /api/v1/confirmations/{id}` until `status` is `executed` — `result` is what the endpoint would have answered, or `problem` why it failed — or `declined`. An expired one answers 410 [`confirmation-expired`](https://impartire.com/problems/confirmation-expired); asking for the same action again while one waits answers 409 [`confirmation-pending`](https://impartire.com/problems/confirmation-pending) with its id. The app cannot confirm, extend or repeat a confirmation; each one runs at most once. The person is shown the app's name, the action in words and every parameter by name — never a bare id. Thirty days after a confirmation is decided its `result` and parameters are cleared; it still reads with its `status`, `summary` and times. ```http HTTP/1.1 202 Accepted Location: /api/v1/confirmations/2b7e4c1a-9d3f-4e8b-a6c5-1f0e9d8c7b6a { "id": "2b7e4c1a-9d3f-4e8b-a6c5-1f0e9d8c7b6a", "status": "pending", "action": "run.signal", "resource": { "type": "run", "id": "0d1e2f3a-4b5c-4d6e-8f7a-9b0c1d2e3f4a", "name": "Internal QC review · S2E3" }, "summary": "Approve step 'Spot-check' on run 'Internal QC review · S2E3'", "client_id": "https://claude.ai/oauth/claude-code-client-metadata", "confirm_url": "https://impartire.com/settings/confirmations/2b7e4c1a-9d3f-4e8b-a6c5-1f0e9d8c7b6a", "created_at": "2026-10-21T12:00:00.000Z", "expires_at": "2026-10-21T12:15:00.000Z", "decided_at": null, "result": null, "problem": null } ``` Over MCP, the guarded tools (`impartire_signal_run`, `impartire_deliver`) answer with the same link in their text, and a client that supports URL elicitation is also asked to open it for the person. ### Connecting Claude Code ```bash claude mcp add --transport http impartire https://impartire.com/mcp ``` Claude Code starts the OAuth step when it first uses the server in a session: you sign in to impartire, choose the workspace and allow. To use an API key instead, add `--header "Authorization: Bearer impk_…"`. ### Connecting Claude or ChatGPT in the browser Add a custom connector (Claude) or an MCP connector (ChatGPT) with the server URL `https://impartire.com/mcp` and OAuth authentication. The app sends you to impartire to sign in, choose the workspace and allow. ## Conventions ### Errors Every error is an RFC 9457 Problem (`application/problem+json`). Branch on `type`, a stable URL `https://impartire.com/problems/`; `detail` says what to do in words; `request_id` is also the `Request-Id` header — quote it when reporting a problem. A 422 lists each invalid field in `errors[]` (`path`, `message`). | Code | Status | Means | | --- | --- | --- | | [`unauthenticated`](https://impartire.com/problems/unauthenticated) | 401 | Unauthenticated | | [`insufficient-scope`](https://impartire.com/problems/insufficient-scope) | 403 | Insufficient scope | | [`insufficient-permission`](https://impartire.com/problems/insufficient-permission) | 403 | Insufficient permission | | [`not-found`](https://impartire.com/problems/not-found) | 404 | Not found | | [`validation`](https://impartire.com/problems/validation) | 422 | Validation failed | | [`idempotency-mismatch`](https://impartire.com/problems/idempotency-mismatch) | 422 | Idempotency key reused | | [`idempotency-in-flight`](https://impartire.com/problems/idempotency-in-flight) | 409 | Request in progress | | [`precondition-failed`](https://impartire.com/problems/precondition-failed) | 412 | Precondition failed | | [`precondition-required`](https://impartire.com/problems/precondition-required) | 428 | Precondition required | | [`type-changed`](https://impartire.com/problems/type-changed) | 409 | Asset type changed | | [`rate-limited`](https://impartire.com/problems/rate-limited) | 429 | Rate limited | | [`conflict`](https://impartire.com/problems/conflict) | 409 | Conflict | | [`legal-hold`](https://impartire.com/problems/legal-hold) | 409 | Under legal hold | | [`unknown-workflow`](https://impartire.com/problems/unknown-workflow) | 422 | Unknown workflow | | [`unknown-asset`](https://impartire.com/problems/unknown-asset) | 422 | Unknown asset | | [`unknown-tag`](https://impartire.com/problems/unknown-tag) | 422 | Unknown tag | | [`invalid-hierarchy`](https://impartire.com/problems/invalid-hierarchy) | 422 | Invalid project hierarchy | | [`collection-constraint`](https://impartire.com/problems/collection-constraint) | 422 | Collection constraint | | [`already-member`](https://impartire.com/problems/already-member) | 409 | Already a member | | [`project-type-disallows`](https://impartire.com/problems/project-type-disallows) | 422 | Project type disallows | | [`confirmation-required`](https://impartire.com/problems/confirmation-required) | 403 | Confirmation required | | [`confirmation-pending`](https://impartire.com/problems/confirmation-pending) | 409 | Confirmation pending | | [`delivery-in-flight`](https://impartire.com/problems/delivery-in-flight) | 409 | Delivery in flight | | [`confirmation-expired`](https://impartire.com/problems/confirmation-expired) | 410 | Confirmation expired | | [`orchestration-unavailable`](https://impartire.com/problems/orchestration-unavailable) | 502 | Orchestration unavailable | | [`playout-unavailable`](https://impartire.com/problems/playout-unavailable) | 502 | Playout unavailable | | [`already-current`](https://impartire.com/problems/already-current) | 422 | Already current | | [`upload-not-landed`](https://impartire.com/problems/upload-not-landed) | 422 | Upload not landed | | [`not-multipart`](https://impartire.com/problems/not-multipart) | 422 | Not a multipart upload | | [`size-mismatch`](https://impartire.com/problems/size-mismatch) | 422 | Size mismatch | | [`storage-unavailable`](https://impartire.com/problems/storage-unavailable) | 503 | Storage unavailable | | [`authorization-unavailable`](https://impartire.com/problems/authorization-unavailable) | 503 | Authorization unavailable | | [`authorization-write-failed`](https://impartire.com/problems/authorization-write-failed) | 500 | Authorization write failed | | [`internal`](https://impartire.com/problems/internal) | 500 | Internal error | ### Idempotency Every creating POST accepts `Idempotency-Key` (1–255 characters; a UUID is usual). A retry with the same key and the same request within 24 hours replays the first answer with `Idempotent-Replayed: true`. The same key with a different method, path or body is 422 [`idempotency-mismatch`](https://impartire.com/problems/idempotency-mismatch); a retry while the first is still running is 409 [`idempotency-in-flight`](https://impartire.com/problems/idempotency-in-flight) with `Retry-After`. Answers that executed nothing (401, 403, 404, 422, 429) are not stored, so the key can be reused with a corrected request. OAuth tokens may not send the header. ### ETags and If-Match A single resource carries an `ETag`; through the CDN it may arrive weak-prefixed (`W/"…"`). Send it back exactly as received — the server compares the value — in `If-None-Match` for a 304. A write to a resource with an `edit_seq` (an asset's or a record's PATCH) requires `If-Match` with the ETag or the bare `edit_seq`: without it, 428 [`precondition-required`](https://impartire.com/problems/precondition-required); stale, 412 [`precondition-failed`](https://impartire.com/problems/precondition-failed) with the current `edit_seq` in the body — GET, reapply, retry. ### Pagination Lists take `limit` (default 50; anything larger is treated as 100) and an opaque `cursor`, and answer `{ data, next_cursor }`; `next_cursor: null` is the last page. A cursor is bound to the other query parameters — change a filter and start again. ### Polling for changes There are no webhooks yet. To keep a copy in step, poll: the lists of assets, projects, collections, a project's playlists, a record type's records, an asset's comments and markers, people, workflows and runs take `updated_after` (ISO 8601 with an offset, exclusive), and every one can be ordered by `updated_at` (`sort=updated_at` or `-updated_at`; runs always come most recently changed first). Remember the newest `updated_at` you have stored, ask for everything changed after it, walk the pages to the end, then store the new high-water mark. Every list compares `updated_after` to the millisecond — the precision timestamps are served in — so the row whose `updated_at` you stored does not come back. Something deleted leaves the list instead of appearing as changed, so reconcile deletions by reading the ids you hold (a 404 means it is gone). A collection's `updated_at` moves with its name, description and constraints, not with its items: compare its `ETag` (or `item_count`) to notice membership changes. A playlist's likewise moves with its name, description and target count, not with its items. An edited, resolved or reopened comment moves its `updated_at`; a deleted comment or marker simply leaves its list. Credits and an asset's categories carry no `updated_at`: re-read them when you re-read the asset or the person. Treat polling as at-least-once, and give it a small overlap. `updated_at` is the database's clock at the start of the write, not at its commit, so a write that started just before another and finished just after it can carry a timestamp slightly behind one you have already stored and be skipped by a strict high-water mark. Ask for `updated_after` a few seconds before your stored mark (say 5 s) and treat a row you already hold at the same `updated_at` as a no-op. ```bash curl -s "https://impartire.com/api/v1/assets?updated_after=2026-10-20T12:00:00Z&sort=updated_at&limit=100" -H "Authorization: Bearer $IMPARTIRE_KEY" curl -s "https://impartire.com/api/v1/projects?updated_after=2026-10-20T12:00:00Z&sort=updated_at" -H "Authorization: Bearer $IMPARTIRE_KEY" ``` ### Rate limits Each credential may make 600 requests per 60 seconds. Every authenticated answer carries `RateLimit-Policy: "default";q=600;w=60`. Over budget is 429 [`rate-limited`](https://impartire.com/problems/rate-limited) with `Retry-After` and `RateLimit`. `GET /api/v1/me` reports the budget. The count is approximate — it is kept per edge location and is eventually consistent, so a burst can exceed the budget before the first 429; pace to the budget rather than relying on the 429 to tell you where it is. ### Request-Id Every answer carries a `Request-Id` header, and every Problem repeats it as `request_id`. ### Versioning `/api/v1` changes only additively: new fields, new optional parameters, new endpoints. Ignore fields you do not know. A breaking change is a new version, announced first on the old one with `Deprecation: @` and `Sunset: ` headers. ## MCP tools The MCP server is `https://impartire.com/mcp` (streamable HTTP). It takes an OAuth token or an API key, and calls the same functions as the API. Every tool answers `response_format: "concise"` by default and `"full"` on request. Nothing that deletes, publishes or shares is a tool; the guarded tools, `impartire_signal_run` and `impartire_deliver`, wait for their person to confirm when called with an OAuth token (with a key, a cancel or a delivery runs at once). | Tool | What it does | Scopes | | --- | --- | --- | | `impartire_whoami` | Which org, person and scopes this connection has. | any | | `impartire_search_assets` | Find assets by name, type, project, tag or external_ref; a page at a time. | `assets:read` | | `impartire_get_asset` | Read one asset: its fields, files (with their technical metadata), recent runs, review counts and whether it can be played. | `assets:read` | | `impartire_list_projects` | List the projects you can file into. | `projects:read` | | `impartire_create_project` | Create a project (optionally typed, under a parent) to file assets into. | `projects:write` | | `impartire_start_upload` | Get a URL to PUT a file's bytes to (step 1 of creating an asset with a file, or of attaching one). | `assets:write` | | `impartire_create_asset` | Create an asset in a project, from an upload or as metadata only. | `assets:write` | | `impartire_update_asset` | Rename an asset, change its fields, set its external_ref, or add and remove tags. | `assets:write` | | `impartire_attach_file` | Add an uploaded file to an existing asset, or replace one of its files as a new version. | `assets:write` | | `impartire_get_download_url` | A five-minute URL for one of an asset's files. | `assets:read` | | `impartire_list_asset_types` | The asset types you can create, with their file slots and field definitions. | `assets:read` | | `impartire_list_workflows` | The workflow templates you can start on an asset, with the parameters each takes. | `workflows:read` | | `impartire_start_run` | Start a workflow run on an asset (never a delivery: use impartire_deliver); answers the run to check on. | `runs:write` | | `impartire_list_runs` | Check one run by id, or list runs by asset and status. | `runs:read` | | `impartire_signal_run` | Approve, reject, complete or skip a run's step, or cancel the run — the person confirms it in impartire first (a key: cancel only). | `runs:signal` | | `impartire_list_collections` | List collections, or read one collection with its items in order. | `collections:read` | | `impartire_add_to_collection` | Add an asset to an existing collection. | `collections:write` | | `impartire_list_records` | List the record types and their fields, a type's records, or read one record. | `records:read` | | `impartire_create_record` | Create a custom record of a record type, reference fields included. | `records:write` | | `impartire_update_record` | Retitle a record or replace its fields (needs the etag from a read). | `records:write` | | `impartire_add_comment` | Comment on an asset, pinned to a moment in the media or as a reply. | `review:write` | | `impartire_deliver` | Deliver an asset's passing file against a delivery requirement — the person confirms it in impartire first (a key: at once). | `delivery:write` | | `impartire_list_audit_log` | Who changed what and when: the org's audit trail, filtered by resource, actor, action or request. | `audit:read` | ## Problems Each `type` is `https://impartire.com/problems/` and resolves to a page. ### unauthenticated (401 Unauthenticated) No credential was presented in the Authorization header, or the API key or OAuth token is unknown, revoked, expired, or for another resource. WWW-Authenticate names the resource_metadata to discover the authorization server from. What to do: Send `Authorization: Bearer `. For a key, check it on Settings → API keys (revoked or expired keys stop at once). For an OAuth token, refresh it, or follow `resource_metadata` in `WWW-Authenticate` and authorize again. ```json { "type": "https://impartire.com/problems/unauthenticated", "title": "Unauthenticated", "status": 401, "detail": "This API key is not recognised: it was revoked, has expired, or was never issued.", "instance": "/api/v1/me", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### insufficient-scope (403 Insufficient scope) The credential is valid but lacks a scope this operation requires; WWW-Authenticate names every missing scope. What to do: Read the `scope` in `WWW-Authenticate`: it names every scope the operation needs. Mint a key with them, or authorize the app again asking for them. ```json { "type": "https://impartire.com/problems/insufficient-scope", "title": "Insufficient scope", "status": 403, "detail": "This operation needs assets:write; the key has assets:read.", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### insufficient-permission (403 Insufficient permission) An OAuth token acts as its person, and they may see this resource but not make this change (or file into that project). Re-authorizing does not help; ask someone who may. What to do: The person the token acts for may not make this change. A broader scope does not help; ask someone who may, or have an admin change the person's access. ```json { "type": "https://impartire.com/problems/insufficient-permission", "title": "Insufficient permission", "status": 403, "detail": "You may see this asset but not change it.", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### not-found (404 Not found) The resource does not exist in this org, or the API is not enabled for it. What to do: Check the id against a list endpoint. A resource in another org, one the token's person cannot see, and an org without the API all answer 404 alike. ```json { "type": "https://impartire.com/problems/not-found", "title": "Not found", "status": 404, "detail": "No asset with this id in this org.", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### validation (422 Validation failed) The request is well-formed but a field is invalid; `errors[]` names each field (`path`) and what is accepted. What to do: Read `errors[]`: each entry names a field (`path`) and what is accepted. Fix those fields and resend; the same `Idempotency-Key` may be reused, since nothing was stored. ```json { "type": "https://impartire.com/problems/validation", "title": "Validation failed", "status": 422, "detail": "project_id is required: an API caller always names its project (GET /api/v1/projects).", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M", "errors": [ { "path": "project_id", "message": "Required: an API caller always names its project (GET /api/v1/projects)." } ] } ``` ### idempotency-mismatch (422 Idempotency key reused) This Idempotency-Key was used with a different request (method, path or body) in the last 24 hours. What to do: Use a new `Idempotency-Key` for a different request. Reuse a key only to retry exactly the same method, path and body. ```json { "type": "https://impartire.com/problems/idempotency-mismatch", "title": "Idempotency key reused", "status": 422, "detail": "This Idempotency-Key was used with a different request in the last 24 hours.", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### idempotency-in-flight (409 Request in progress) A request with this Idempotency-Key is still being processed; retry after `Retry-After` seconds. What to do: Wait `Retry-After` seconds and send the same request with the same key; you will get the first request's answer. ```json { "type": "https://impartire.com/problems/idempotency-in-flight", "title": "Request in progress", "status": 409, "detail": "A request with this Idempotency-Key is still being processed; retry after Retry-After seconds.", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### precondition-failed (412 Precondition failed) If-Match did not match the resource's current `edit_seq`; the body carries the current one. What to do: GET the resource, reapply your change to what it holds now, and send its current ETag (or the `edit_seq` in this body) as `If-Match`. ```json { "type": "https://impartire.com/problems/precondition-failed", "title": "Precondition failed", "status": 412, "detail": "If-Match does not match the asset's current edit_seq (7). GET it, reapply your change, and retry.", "instance": "/api/v1/assets/6f1c2a4e-0b7d-4f7e-9a51-2d3c4b5a6e7f", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M", "edit_seq": 7 } ``` ### precondition-required (428 Precondition required) This write needs an If-Match header carrying the ETag (or the bare `edit_seq`) of the version being changed; GET the resource first. What to do: GET the resource and send its `ETag` (or its bare `edit_seq`) as `If-Match` with the write. ```json { "type": "https://impartire.com/problems/precondition-required", "title": "Precondition required", "status": 428, "detail": "Send If-Match with the asset's ETag (or its edit_seq) from a GET.", "instance": "/api/v1/assets/6f1c2a4e-0b7d-4f7e-9a51-2d3c4b5a6e7f", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### type-changed (409 Asset type changed) The asset type's field schema changed since the `schema_version` the request was built against, and the fields no longer validate; GET the asset type and rebuild the fields. What to do: GET the asset type, rebuild `fields` against its current schema, and send the new `schema_version` with the change. ```json { "type": "https://impartire.com/problems/type-changed", "title": "Asset type changed", "status": 409, "detail": "The asset type's schema is now version 4; the fields no longer validate. GET the type and rebuild them.", "instance": "/api/v1/assets/6f1c2a4e-0b7d-4f7e-9a51-2d3c4b5a6e7f", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M", "schema_version": 4 } ``` ### rate-limited (429 Rate limited) This key has used its request budget for the window; wait `Retry-After` seconds. `RateLimit-Policy` on every response states the budget. What to do: Wait `Retry-After` seconds. `RateLimit-Policy` on every answer states the budget; spread requests across the window. ```json { "type": "https://impartire.com/problems/rate-limited", "title": "Rate limited", "status": 429, "detail": "This key has used its 600 requests for the window; retry after Retry-After seconds.", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### conflict (409 Conflict) The request conflicts with the resource's current state. What to do: Read `detail` for what conflicts; GET the resource's current state before trying again. ```json { "type": "https://impartire.com/problems/conflict", "title": "Conflict", "status": 409, "detail": "The request conflicts with the resource's current state.", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### legal-hold (409 Under legal hold) The asset is under legal hold, so it cannot be deleted, given a new file version (attach, replace, restore), delivered or scheduled to air, and a step of its runs cannot be signed off, rejected or skipped; `detail` gives the hold's reason. Only someone who may release the hold can lift it, in the app. What to do: Leave the asset in place. A hold is lifted in the app by someone who may release it; retrying the delete (or the attach, replace, restore, delivery or step sign-off) will not succeed until then. ```json { "type": "https://impartire.com/problems/legal-hold", "title": "Under legal hold", "status": 409, "detail": "This asset is under legal hold and cannot be deleted: litigation hold, matter 2026-114.", "instance": "/api/v1/assets/6f1c2a4e-0b7d-4f7e-9a51-2d3c4b5a6e7f", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### unknown-workflow (422 Unknown workflow) `workflow_id` names no live workflow template this org can start; GET /api/v1/workflows lists them. What to do: List the templates with `GET /api/v1/workflows` and pass one's `id`. A template whose feature the org does not have is not listed and cannot be started. ```json { "type": "https://impartire.com/problems/unknown-workflow", "title": "Unknown workflow", "status": 422, "detail": "workflow_id names no workflow template this org can start. GET /api/v1/workflows lists them.", "instance": "/api/v1/runs", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### unknown-asset (422 Unknown asset) `asset_id` names no live asset of this org that the credential can read; GET /api/v1/assets lists them. What to do: Check the id with `GET /api/v1/assets/{id}`; a deleted asset, one in another org, and (for an OAuth token) one the person cannot read all answer alike. ```json { "type": "https://impartire.com/problems/unknown-asset", "title": "Unknown asset", "status": 422, "detail": "asset_id names no asset of this org. GET /api/v1/assets lists them.", "instance": "/api/v1/runs", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### unknown-tag (422 Unknown tag) A tag `id` names no tag of this org that the credential can read; GET /api/v1/tags lists them, and `{ name }` coins a new org tag instead. What to do: Look the tag up with `GET /api/v1/tags?q=`; a tag of another org and (for an OAuth token) a team tag the person is not in answer alike. To coin a new org tag, send `{ "name" }` instead of `{ "id" }`. ```json { "type": "https://impartire.com/problems/unknown-tag", "title": "Unknown tag", "status": 422, "detail": "tags.0.id names no tag of this org you can use. GET /api/v1/tags lists them; to coin a new org tag, send { \"name\" } instead.", "instance": "/api/v1/assets/6f1c2a4e-0b7d-4f7e-9a51-2d3c4b5a6e7f/tags", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### invalid-hierarchy (422 Invalid project hierarchy) The org's project hierarchy rules do not allow this: the project type is not allowed under its parent's type (or at the root, or above a sub-project's type), or the tree would be more than five levels deep. `detail` names the rule; the rules are set in the app. What to do: Pick a project type the org's rules allow at that place, or another parent. The rules (which type may hold which) are set in the app under Projects → Project Hierarchy; an untyped project may go anywhere. ```json { "type": "https://impartire.com/problems/invalid-hierarchy", "title": "Invalid project hierarchy", "status": 422, "detail": "The org's hierarchy rules do not allow a project of type \"Episode\" at the root. Give it a parent_id whose type may hold it, or pick another type.", "instance": "/api/v1/projects", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### collection-constraint (422 Collection constraint) The collection does not accept this asset: its type is not in `allowed_type_ids`, it carries none of `allowed_category_ids`, or the collection is published and the asset is not. `errors[].path` names the rule; GET the collection to see its constraints. What to do: Read `errors[].path` for the rule: `allowed_type_ids` (add an asset of an allowed type, or widen the list with PATCH /api/v1/collections/{id}), `allowed_category_ids` (assign the asset one of the categories in the app), or `published` (the collection is published; publish the asset first, in the app). ```json { "type": "https://impartire.com/problems/collection-constraint", "title": "Collection constraint", "status": 422, "detail": "This asset's type is not allowed in this collection. GET /api/v1/collections/5a7c1e2d-3b4f-4a6e-9d8c-7b6a5f4e3d2c shows its allowed_type_ids and allowed_category_ids.", "instance": "/api/v1/collections/5a7c1e2d-3b4f-4a6e-9d8c-7b6a5f4e3d2c/items", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M", "errors": [ { "path": "allowed_type_ids", "message": "This asset's type is not allowed in this collection" } ] } ``` ### already-member (409 Already a member) The asset is already an item of this collection or playlist (an asset appears in one once); `item_id` in the body is the existing item. What to do: Nothing to add: the asset is already in the collection or playlist, as the item `item_id`. To move it, PUT …/items/order on that collection or playlist. ```json { "type": "https://impartire.com/problems/already-member", "title": "Already a member", "status": 409, "detail": "Asset 6f1c2a4e-0b7d-4f7e-9a51-2d3c4b5a6e7f is already in collection 5a7c1e2d-3b4f-4a6e-9d8c-7b6a5f4e3d2c (item 8e9f0a1b-2c3d-4e5f-8a6b-7c8d9e0f1a2b).", "instance": "/api/v1/collections/5a7c1e2d-3b4f-4a6e-9d8c-7b6a5f4e3d2c/items", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M", "item_id": "8e9f0a1b-2c3d-4e5f-8a6b-7c8d9e0f1a2b" } ``` ### project-type-disallows (422 Project type disallows) The project's type does not allow this (a playlist in a project whose type has playlists turned off). Pick another project, or change the type's settings in the app. What to do: Pick a project whose type allows playlists (an untyped project always does; GET /api/v1/projects shows each `project_type`), or turn playlists on for the type in the app under Projects → Project Types. ```json { "type": "https://impartire.com/problems/project-type-disallows", "title": "Project type disallows", "status": 422, "detail": "Project 66b0c1d2-3e4f-4a5b-8c6d-7e8f9a0b1c2d's type does not allow playlists. Pick a project whose type allows them, or turn playlists on for the type in the app.", "instance": "/api/v1/playlists", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M", "errors": [ { "path": "project_id", "message": "the project's type does not allow playlists" } ] } ``` ### confirmation-required (403 Confirmation required) The workflow template reaches a delivery — its own action, a step's, or a sub-workflow's — which POST /runs never starts, with any credential. Deliver against a requirement with POST /api/v1/delivery-requirements/{id}/deliver (`delivery:write`, a guarded action); publish to a social platform in the app. What to do: A template that reaches a delivery (itself, a step, or a sub-workflow) is never started through `POST /runs`. Deliver against a requirement with `POST /api/v1/delivery-requirements/{id}/deliver` and `delivery:write` (an OAuth token's person confirms it in impartire); publish to a social platform in the app. ```json { "type": "https://impartire.com/problems/confirmation-required", "title": "Confirmation required", "status": 403, "detail": "\"Deliver file\" delivers content out of the org, which POST /runs does not start: deliver against a requirement with POST /api/v1/delivery-requirements/{id}/deliver, or start it in the app.", "instance": "/api/v1/runs", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### confirmation-pending (409 Confirmation pending) The same action, on the same resource with the same parameters, is already waiting for its person to confirm it in impartire; `confirmation_id` and `confirm_url` name it. Poll GET /api/v1/confirmations/{id} instead of asking again. What to do: Do not ask again: the person has not decided yet. Show them `confirm_url`, then poll `GET /api/v1/confirmations/{confirmation_id}` until its `status` is `executed`, `declined` or `expired`. ```json { "type": "https://impartire.com/problems/confirmation-pending", "title": "Confirmation pending", "status": 409, "detail": "This action is already waiting for its person to confirm it (confirmation 2b7e4c1a-9d3f-4e8b-a6c5-1f0e9d8c7b6a). Send them its confirm_url and poll it; do not ask again.", "instance": "/api/v1/runs/0d1e2f3a-4b5c-4d6e-8f7a-9b0c1d2e3f4a/signals", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M", "confirmation_id": "2b7e4c1a-9d3f-4e8b-a6c5-1f0e9d8c7b6a", "confirm_url": "https://impartire.com/settings/confirmations/2b7e4c1a-9d3f-4e8b-a6c5-1f0e9d8c7b6a" } ``` ### delivery-in-flight (409 Delivery in flight) The same file is already being delivered to the same destination for the same requirement; `receipt_id` and `run_id` name that delivery's receipt and run. Wait for the run to end before delivering again; a run parked as an incident stays active, so cancel it if it is stuck. What to do: Do not deliver again yet. `run_id` is the run pushing it: poll `GET /api/v1/runs/{run_id}` (or the receipt, `GET /api/v1/delivery-receipts/{receipt_id}`) until it ends. A run parked as an incident stays `active` and keeps blocking; if it is stuck, cancel it (`POST /api/v1/runs/{run_id}/signals` with `cancel`, or in the app) and deliver again — a receipt whose run ended never blocks. ```json { "type": "https://impartire.com/problems/delivery-in-flight", "title": "Delivery in flight", "status": 409, "detail": "This file is already being delivered to this destination for this requirement (receipt 7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a, run 2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b). Wait for that run to finish; if it is stuck (an incident keeps it active), cancel it, then deliver again.", "instance": "/api/v1/delivery-requirements/4c5d6e7f-8a9b-4c0d-8e1f-2a3b4c5d6e7f/deliver", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M", "receipt_id": "7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a", "run_id": "2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b" } ``` ### confirmation-expired (410 Confirmation expired) Nobody confirmed the action within 15 minutes, so it will never run. Ask again for a new confirmation if it is still wanted. What to do: The action did not run and never will. If it is still wanted, make the same request again: it answers 202 with a new confirmation for the person to confirm within 15 minutes. ```json { "type": "https://impartire.com/problems/confirmation-expired", "title": "Confirmation expired", "status": 410, "detail": "Confirmation 2b7e4c1a-9d3f-4e8b-a6c5-1f0e9d8c7b6a expired before anyone confirmed it, so the action did not run. Ask again if it is still wanted.", "instance": "/api/v1/confirmations/2b7e4c1a-9d3f-4e8b-a6c5-1f0e9d8c7b6a", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### orchestration-unavailable (502 Orchestration unavailable) The run could not be handed to the orchestrator, so nothing was started; retry later (with the same Idempotency-Key). What to do: Nothing was started. Retry later; with the same `Idempotency-Key` the retry is safe. ```json { "type": "https://impartire.com/problems/orchestration-unavailable", "title": "Orchestration unavailable", "status": 502, "detail": "The run could not be handed to the orchestrator, so nothing was started. Retry later.", "instance": "/api/v1/runs", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### playout-unavailable (502 Playout unavailable) The playout engine could not be reached or did not start or stop the channel, so nothing changed on air; retry later (with the same Idempotency-Key). What to do: Nothing changed on air. Retry later; with the same `Idempotency-Key` the retry is safe. `GET /channels/{id}` shows the channel. ```json { "type": "https://impartire.com/problems/playout-unavailable", "title": "Playout unavailable", "status": 502, "detail": "The playout engine did not start the channel, so nothing changed on air. Retry later; GET /api/v1/channels/5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d shows the channel.", "instance": "/api/v1/channels/5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d/start", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### already-current (422 Already current) The file version named to restore is already its chain's current version, so there is nothing to restore; GET /api/v1/assets/{id}/files/{file_id} lists the chain's versions. What to do: Nothing to do: that version is the file's current one. To bring back an earlier version, restore one whose `is_current` is false in the file's `versions`. ```json { "type": "https://impartire.com/problems/already-current", "title": "Already current", "status": 422, "detail": "File 3fb8b1ca-0d6e-4a59-9b1e-8c2f7d4e5a61 is already the current version of its chain; restore an earlier version from its versions.", "instance": "/api/v1/assets/6f1c2a4e-0b7d-4f7e-9a51-2d3c4b5a6e7f/files/3fb8b1ca-0d6e-4a59-9b1e-8c2f7d4e5a61/restore", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### upload-not-landed (422 Upload not landed) The upload's bytes have not reached storage yet; PUT them to `upload_url` first (a multipart upload: PUT every part, then POST /api/v1/uploads/{id}/complete). What to do: PUT the bytes to `upload_url` with the returned `headers` first (for a multipart upload, PUT every part and POST `/api/v1/uploads/{id}/complete`), then create the asset. `GET /api/v1/uploads/{id}` shows whether they landed. ```json { "type": "https://impartire.com/problems/upload-not-landed", "title": "Upload not landed", "status": 422, "detail": "The upload's bytes have not reached storage yet; PUT them to upload_url first.", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### not-multipart (422 Not a multipart upload) The upload is a single PUT, so it has no parts to sign, complete or abort; PUT the whole file to its `upload_url`. POST /api/v1/uploads with `multipart: true` starts a multipart upload. What to do: This upload is a single PUT: PUT the whole file to its `upload_url`. For a file over 5 GiB, start a new upload with `multipart: true`. ```json { "type": "https://impartire.com/problems/not-multipart", "title": "Not a multipart upload", "status": 422, "detail": "Upload 9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f is a single PUT, not a multipart upload: PUT the whole file to its upload_url.", "instance": "/api/v1/uploads/9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f/parts", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### size-mismatch (422 Size mismatch) The completed multipart upload is not `size_bytes` long, so it was discarded and the upload failed; start a new upload with the file's exact size. What to do: The parts were discarded. Start a new upload with the file's exact `size_bytes` and send every part of exactly `part_size_bytes` (the last part: the rest). ```json { "type": "https://impartire.com/problems/size-mismatch", "title": "Size mismatch", "status": 422, "detail": "The assembled file is 52428800 bytes, not the 52428801 this upload was created for, so it was discarded.", "instance": "/api/v1/uploads/9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f/complete", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### storage-unavailable (503 Storage unavailable) The org's storage could not be reached; retry later. What to do: Retry later, with the same `Idempotency-Key` on a POST. ```json { "type": "https://impartire.com/problems/storage-unavailable", "title": "Storage unavailable", "status": 503, "detail": "The org's storage could not be reached; retry later.", "instance": "/api/v1/uploads", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### authorization-unavailable (503 Authorization unavailable) The permission service could not be reached, so a change to who has access did not happen; retry later. What to do: Nothing changed. Retry later; with the same `Idempotency-Key` a retried POST is safe. ```json { "type": "https://impartire.com/problems/authorization-unavailable", "title": "Authorization unavailable", "status": 503, "detail": "The permission service could not be reached, so nothing changed. Retry later.", "instance": "/api/v1/projects/66b0c1d2-3e4f-4a5b-8c6d-7e8f9a0b1c2d/access", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### authorization-write-failed (500 Authorization write failed) The write landed but its permission tuples could not be written, so it was rolled back (a create is soft-deleted, a move is reverted). On a create, retrying with the same Idempotency-Key replays this answer and a new key retries; a move can simply be retried. What to do: Retry. For a create, a new `Idempotency-Key` retries; the same key replays this answer. ```json { "type": "https://impartire.com/problems/authorization-write-failed", "title": "Authorization write failed", "status": 500, "detail": "The asset could not be made visible and was rolled back. Retry with a new Idempotency-Key.", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ### internal (500 Internal error) Something failed on our side. The response carries no detail; quote `request_id` when reporting it. What to do: Retry later. If it persists, report it with the `request_id`. ```json { "type": "https://impartire.com/problems/internal", "title": "Internal error", "status": 500, "detail": "An internal error occurred. Quote request_id if you report it.", "instance": "/api/v1/assets", "request_id": "01JB8Z5W6Q4N3X2V1T0S9R8P7M" } ``` ## Reference Every path is under `https://impartire.com/api/v1`. The machine-readable contract is https://impartire.com/api/v1/openapi.json (OpenAPI 3.1). ### The contract #### GET /api/v1/openapi.json This document Scopes: none (public) Other responses: - 200 — The OpenAPI 3.1 document. ### Who is calling #### GET /api/v1/me Who is calling: the key, its org, its scopes and its request budget Any valid key may call it, whatever its scopes. Scopes: any valid credential Response 200 — `Me`: Who is calling: the credential, its org, its authority, its scopes and its request budget. - `authority` ("org" | "user", required) — `org`: an org API key — its scopes, org-wide. `user`: an OAuth access token acting for one person — its scopes AND that person's own permissions, so it sees what they see and does what they may do. - `org` (object, required) — The org this credential acts in. - `id` (string (uuid), required) - `name` (string, required) - `credential_class` ("standard" | "guarded", required) — `guarded` when the credential holds any guarded scope (runs:signal, delivery:write, schedules:write, channels:control, access:write, sharing:write); `standard` otherwise. A guarded key must expire within 90 days and acts at once. With a guarded token, each guarded action waits for its person to confirm it in impartire. - `key` (object or null, required) — The API key, for `authority: org`; null for a token. - `id` (string (uuid), required) - `name` (string, required) - `scopes` (array of string, required) — What this key may do. `:write` includes `:read`. - `expires_at` (string (date-time) or null, required) — When the key stops working, or null if it does not expire. - `created_at` (string (date-time), required) - `user` (object or null, required) — The person a token acts for, for `authority: user`; null for a key. - `id` (string, required) - `name` (string, required) - `email` (string, required) - `client_id` (string or null, required) — The OAuth client acting for the person (a URL for a client that published a metadata document); null for a key. - `scopes` (array of string, required) — What this credential may do. `:write` includes `:read`. - `rate_limit` (object, required) - `limit` (integer, required) — Requests allowed per window, per credential. Enforced approximately (per edge location, eventually consistent): a burst can exceed it before the first 429. - `window_seconds` (integer, required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Asset types #### GET /api/v1/asset-types List the asset types the org can create assets of, with their field definitions and file slots Each type's `fields` is the validation contract for `fields` on POST and PATCH /assets: a declared key must hold a value of its type. Scopes: `assets:read` Response 200 — `AssetTypeList` - `data` (array of AssetType, required) - `id` (string (uuid), required) - `name` (string, required) - `kind` ("asset", required) - `fields` (array of AssetField, required) — The type's live field definitions, in the app's form order. Deprecated fields, and for an OAuth token the fields its person may not read, are left out. - `id` (string (uuid), required) - `key` (string, required) — The key a value is stored under in an asset's `fields`. - `name` (string, required) — The field's label, as the app shows it. - `type` (string, required) — text | url (a string) · select (one option `value`) · multiselect (an array of option values) · date (YYYY-MM-DD) · datetime (epoch milliseconds) · number · reference (an id, or an array of ids when `multiple`). More may be added. - `required` (boolean, required) — Required for the asset to be ready (a workflow gate may wait on it). Not enforced on a write: an asset may be created empty and filled in later. - `multiple` (boolean, required) — For a reference field, whether the value is an array of ids. - `options` (array of object or null, required) — For select and multiselect, the accepted values (store `value`; `label` is for display). Null for every other type. - `value` (string, required) - `label` (string, required) - `file_slots` (array of FileSlot, required) - `id` (string (uuid), required) - `name` (string or null, required) - `media_type` (string, required) — video | image | audio | document | subtitle - `accept_extensions` (array of string or null, required) — Accepted file extensions, or null for any of the media type. - `multiple` (boolean, required) — Whether the slot holds several files. - `next_cursor` (string or null, required) — Always null: the list is returned whole. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Projects #### GET /api/v1/projects List the org's projects Ordered by name unless `sort` says otherwise. To poll for changes, pass `updated_after` with the newest `updated_at` you have seen and `sort=-updated_at` (or `updated_at`). A deleted project leaves the list. Scopes: `projects:read` Parameters: - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. - `parent_id` (query, string (uuid)) — Only the direct children of this project. - `updated_after` (query, string (date-time)) — Only projects changed strictly after this instant (ISO 8601 with offset), compared to the millisecond — poll with the newest `updated_at` you have seen (that project does not come back), sorted `-updated_at` or `updated_at`. - `sort` (query, "name" | "-name" | "updated_at" | "-updated_at") — Sort key; a leading `-` is descending. Default `name`. Ties break on id. Response 200 — `ProjectList` - `data` (array of Project, required) - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `parent_id` (string (uuid) or null, required) - `visibility` (string, required) — open | closed | private - `project_type` (object or null, required) — The project's type (the org's project types: production, season, campaign…), or null if untyped. - `id` (string (uuid), required) - `name` (string, required) - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or type last changed. - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit`, `parent_id`, `updated_after`, `sort` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/projects Create a project, optionally typed and under a parent A root project (no `parent_id`) is born Closed — org members see that it exists, admitted teams open it; a sub-project inherits its parent's access. The credential is the project's owner: for an API key, the key's service principal. Typed projects follow the org's hierarchy rules. Scopes: `projects:write` Parameters: - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CreateProjectRequest` - `name` (string, required) - `description` (string or null) - `parent_id` (string (uuid)) — Create it as a sub-project of this project (GET /api/v1/projects). Omit for a root project (a production), which is born Closed: org members see it exists, admitted teams open it. - `project_type_id` (string (uuid)) — One of the org's project types. When the parent is typed too, the org's hierarchy rules must allow this type under the parent's (a root needs a rule allowing the type at the root); 422 invalid-hierarchy otherwise. Response 201 — `Project` - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `parent_id` (string (uuid) or null, required) - `visibility` (string, required) — open | closed | private - `project_type` (object or null, required) — The project's type (the org's project types: production, season, campaign…), or null if untyped. - `id` (string (uuid), required) - `name` (string, required) - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or type last changed. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 409 (Problem) — idempotency-in-flight. - 422 (Problem) — validation (name, description, parent_id, project_type_id), invalid-hierarchy, or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — authorization-write-failed — the project's permissions could not be written, so it was rolled back; or internal. #### GET /api/v1/projects/{id} Get one project Scopes: `projects:read` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `Project` - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `parent_id` (string (uuid) or null, required) - `visibility` (string, required) — open | closed | private - `project_type` (object or null, required) — The project's type (the org's project types: production, season, campaign…), or null if untyped. - `id` (string (uuid), required) - `name` (string, required) - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or type last changed. Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### PATCH /api/v1/projects/{id} Rename, describe or retype a project If-Match is required: send the ETag from the last GET (or from the create). A retype must fit the org's hierarchy rules under the parent's type and above each sub-project's type. Moving a project is POST /projects/{id}/move (guarded); changing its visibility is done in the app. Scopes: `projects:write` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. - `If-Match` (header, string, required) — The project's ETag. Request body — `UpdateProjectRequest`: At least one of name, description, project_type_id. Moving a project and changing its visibility are not in v1. - `name` (string) - `description` (string or null) — null clears it. - `project_type_id` (string (uuid) or null) — Retype the project; null makes it untyped. The org's hierarchy rules must allow the type under its parent's type (or at the root) and above each sub-project's type; 422 invalid-hierarchy otherwise. Response 200 — `Project` - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `parent_id` (string (uuid) or null, required) - `visibility` (string, required) — open | closed | private - `project_type` (object or null, required) — The project's type (the org's project types: production, season, campaign…), or null if untyped. - `id` (string (uuid), required) - `name` (string, required) - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or type last changed. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 412 (Problem) — precondition-failed — If-Match is stale; `etag` in the body is the current one. - 422 (Problem) — validation (empty body, name, description, project_type_id), or invalid-hierarchy. - 428 (Problem) — precondition-required — no If-Match. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/projects/{id}/access Give a member, team or role access to a project (guarded) A guarded action (ADR 0081): it changes who can see the project. The app's own write runs, with its checks and words. **An API key's write happens at once.** **An OAuth token's does not**: 202 with a Confirmation naming the project and who, by name; once its person confirms it in impartire it runs as them, every check again. Its person must manage the project (project manage; no platform-admin bypass). The subject must be a member, team or role of the org (422), a team not archived and a subject not denied on the project (409: lift the deny first). A team's or role's grant follows its membership. Audited: a `project.access_granted` event. Scopes: `access:write` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `GrantProjectAccessRequest` - `subject_type` ("member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership. - `subject_id` (string (uuid), required) — The member's principal id (GET /api/v1/members), or the team's or role's id — of this org. - `level` ("viewer" | "asset_contributor" | "asset_purger" | "contributor" | "manager", required) — viewer: read the project and everything filed under it. contributor: edit the project too. manager: administer it, including who has access. asset_contributor: edit the assets filed under it WITHOUT reading the project. asset_purger: delete them, likewise. Response 201 — `ProjectGrant`: A project access grant: who, at which level. - `project` (object, required) — The project. - `id` (string (uuid), required) - `name` (string, required) - `subject` (object, required) - `type` ("member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership. - `id` (string (uuid), required) — The member's principal id, or the team's or role's id. - `name` (string or null, required) — The member's name, or the team's or role's. Null when it is no longer in the org. - `level` ("viewer" | "asset_contributor" | "asset_purger" | "contributor" | "manager", required) — viewer: read the project and everything filed under it. contributor: edit the project too. manager: administer it, including who has access. asset_contributor: edit the assets filed under it WITHOUT reading the project. asset_purger: delete them, likewise. Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person can open the project but not manage it. - 404 (Problem) — not-found. - 409 (Problem) — conflict — an archived team, or a subject denied on the project; confirmation-pending; or idempotency-in-flight. - 422 (Problem) — validation (a bad field; `subject_id` not of the org), or idempotency-mismatch (an OAuth token may not send Idempotency-Key). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — authorization-unavailable — the permission service could not be reached; nothing changed. #### DELETE /api/v1/projects/{id}/access Take one access grant on a project away (guarded) A guarded action (ADR 0081): it changes who can see the project. The app's own write runs, with its checks and words. **An API key's write happens at once.** **An OAuth token's does not**: 202 with a Confirmation naming the project and who, by name; once its person confirms it in impartire it runs as them, every check again. Its person must manage the project. Names the grant by `subject_type`, `subject_id` and `level`; a subject that has since left the org can still be revoked. 404 when there is no such grant (nothing changed). Audited: a `project.access_revoked` event. Scopes: `access:write` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. - `subject_type` (query, "member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership. - `subject_id` (query, string (uuid), required) — The subject the grant names. - `level` (query, "viewer" | "asset_contributor" | "asset_purger" | "contributor" | "manager", required) — viewer: read the project and everything filed under it. contributor: edit the project too. manager: administer it, including who has access. asset_contributor: edit the assets filed under it WITHOUT reading the project. asset_purger: delete them, likewise. Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 204 — An API key's revoke: the grant is gone. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person cannot manage the project. - 404 (Problem) — not-found. - 409 (Problem) — confirmation-pending. - 422 (Problem) — validation — a missing or bad `subject_type`, `subject_id` or `level`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — authorization-unavailable — the permission service could not be reached; nothing changed. #### POST /api/v1/projects/{id}/deny Deny a member, team or role on a project (guarded) A guarded action (ADR 0081): it changes who can see the project. The app's own write runs, with its checks and words. **An API key's write happens at once.** **An OAuth token's does not**: 202 with a Confirmation naming the project and who, by name; once its person confirms it in impartire it runs as them, every check again. Its person must manage the project. A deny removes read — and everything above it — on the project and everything under it, whatever else grants it, admins included (ADR 0047). A `reason` is required. Not yourself (422: you could not lift it); the subject must be the org's (422); already denied is 409. Audited: a `project.access_denied` event (its reason is not served by the audit log). Scopes: `access:write` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `DenyProjectAccessRequest` - `subject_type` ("member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership. - `subject_id` (string (uuid), required) — The member's principal id, or the team's or role's id — of this org. Not yourself. - `reason` (string, required) — Why. Required: a deny is a reasoned, recorded act. Response 201 — `ProjectDeny`: A deny: the subject loses read (and everything above it) on the project and everything under it, whatever else grants it. - `project` (object, required) — The project. - `id` (string (uuid), required) - `name` (string, required) - `subject` (object, required) - `type` ("member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership. - `id` (string (uuid), required) — The member's principal id, or the team's or role's id. - `name` (string or null, required) — The member's name, or the team's or role's. Null when it is no longer in the org. - `reason` (string, required) Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person cannot manage the project. - 404 (Problem) — not-found. - 409 (Problem) — conflict — already denied; confirmation-pending; or idempotency-in-flight. - 422 (Problem) — validation (a bad field, yourself, a subject not of the org), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — authorization-unavailable — the permission service could not be reached; nothing changed. #### DELETE /api/v1/projects/{id}/deny Lift a deny on a project (guarded) A guarded action (ADR 0081): it changes who can see the project. The app's own write runs, with its checks and words. **An API key's write happens at once.** **An OAuth token's does not**: 202 with a Confirmation naming the project and who, by name; once its person confirms it in impartire it runs as them, every check again. Its person must manage the project. The deny's record is kept, marked lifted. 404 when the subject has no active deny. Audited: a `project.deny_lifted` event. Scopes: `access:write` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. - `subject_type` (query, "member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership. - `subject_id` (query, string (uuid), required) — The denied subject. Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 204 — An API key's lift: the deny no longer applies. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person cannot manage the project. - 404 (Problem) — not-found. - 409 (Problem) — confirmation-pending. - 422 (Problem) — validation — a missing or bad `subject_type` or `subject_id`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — authorization-unavailable — the deny is recorded as lifted but may still be enforced; deny again, then lift once the service is back. #### POST /api/v1/projects/{id}/teams Assign a team to a production (guarded) A guarded action (ADR 0081): it changes who can see the project. The app's own write runs, with its checks and words. **An API key's write happens at once.** **An OAuth token's does not**: 202 with a Confirmation naming the project and who, by name; once its person confirms it in impartire it runs as them, every check again. Its person must administer the production (project administrate). The project must be a top-level project nobody owns and not itself a lane; the team the org's and not archived (422). The team's members read the production and get their own lane on it — a project the team owns, private to it. Idempotent: 201 when the lane was made, 200 when the team was already assigned. There is no unassign in the API (nor in the app). Audited: a `project.team_bound` event and the lane's capture. Scopes: `access:write` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `BindProjectTeamRequest` - `team_id` (string (uuid), required) — A team of this org that is not archived. Response 200 — `ProductionLane`: A team assigned to a production, and its lane. - `production` (object, required) — The production (the project). - `id` (string (uuid), required) - `name` (string, required) - `team` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `lane` (object, required) — The team's lane on the production: a project the team owns, private to it, attached to the production without inheriting its audience. - `id` (string (uuid), required) - `name` (string, required) - `created` (boolean, required) — False when the team was already assigned (the lane is the existing one). Response 201 — `ProductionLane`: A team assigned to a production, and its lane. - `production` (object, required) — The production (the project). - `id` (string (uuid), required) - `name` (string, required) - `team` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `lane` (object, required) — The team's lane on the production: a project the team owns, private to it, attached to the production without inheriting its audience. - `id` (string (uuid), required) - `name` (string, required) - `created` (boolean, required) — False when the team was already assigned (the lane is the existing one). Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person does not administer the production. - 404 (Problem) — not-found. - 409 (Problem) — confirmation-pending, or idempotency-in-flight. - 422 (Problem) — validation — `team_id` not a team of the org, an archived team, or a project that cannot be a production; idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — authorization-unavailable — the permission service could not be reached; retry (assigning again is safe). #### POST /api/v1/projects/{id}/move Move a project under another, or to the top level (guarded) A guarded action (ADR 0081): a project reads through its permission root, so moving it changes who can see it. The app's own move runs: Unfiled, the Library, a lane (nesting) and a team space (nesting) stay put (409); the parent must be a project of the org (422), not inside the moved subtree, allowed by the org's hierarchy rules and no more than five levels deep (422 invalid-hierarchy); and a move may narrow or keep the project's audience, never widen it (422 — widen a root from its settings page in the app). A move that narrows it revokes the public share links to the assets under it. Its person must be able to update the project and the new parent (no platform-admin bypass). **An API key's move happens at once**: 200 with the Project. **An OAuth token's does not**: 202 with a Confirmation saying where it goes and what that does; once its person confirms it in impartire it runs as them, every check again. Moving a project to where it already is answers 200 at once, for either credential. Audited: a `project.moved` event and the row's capture. Scopes: `access:write` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `MoveProjectRequest` - `parent_id` (string (uuid) or null, required) — The new parent project (GET /api/v1/projects), or null to make it a top-level project. Response 200 — `Project` - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `parent_id` (string (uuid) or null, required) - `visibility` (string, required) — open | closed | private - `project_type` (object or null, required) — The project's type (the org's project types: production, season, campaign…), or null if untyped. - `id` (string (uuid), required) - `name` (string, required) - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or type last changed. Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person cannot update the project or the new parent. - 404 (Problem) — not-found. - 409 (Problem) — conflict — an infrastructure project; confirmation-pending; or idempotency-in-flight. - 422 (Problem) — validation (`parent_id` not a project of the org, its own subtree, or a move that would widen who can see it), invalid-hierarchy, or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — authorization-unavailable — the permission service could not be reached; nothing changed. #### GET /api/v1/projects/{id}/tags List the tags on a project A project's own tags: its sub-projects and assets do not inherit them. At most 500, ordered by the tag's normalized name; no cursor. Scopes: `projects:read` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. Response 200 — `TagLinkList` - `data` (array of TagLink, required) — Ordered by the tag's normalized name. At most 500. - `id` (string (uuid), required) — The tag's id — what DELETE …/tags/{tag_id} and `{ id }` on POST …/tags take. - `name` (string, required) — The tag as first typed. Tags are matched by a normalized form: case, extra spaces and invisible characters are ignored. - `scope` ("org" | "team", required) — `org`: the whole org's. `team`: private to one team (an OAuth token sees it only when its person is in the team or an org admin). - `team` (object or null, required) — The owning team, for a team tag; null for an org tag. - `id` (string (uuid), required) - `name` (string, required) - `usage_count` (integer, required) — How many assets and projects carry the tag, across the org. - `source` ("user" | "generated", required) — `user`: a person or an API credential added it. `generated`: applied from a generated summary. - `added_at` (string (date-time), required) - `next_cursor` (string or null, required) — Always null: the list is returned whole (at most 500). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/projects/{id}/tags Add tags to a project Each item is `{ name }` — the org's tag of that name, created as an org tag when there is none — or `{ id }` of an existing tag (the only way to add a team tag). A tag already on the project stays as it was, so the call is safe to repeat: no Idempotency-Key, no If-Match. Answers the project's whole tag list after. An OAuth token's person needs update on the project. Scopes: `projects:write` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. Request body — `AddTagsRequest` - `tags` (array of object or object, required) — 1–50 tags, each `{ name }` or `{ id }`. - `name` (string, required) — A tag name. The org's tag of that name is used, or created as an org tag if there is none. Response 200 — `TagLinkList` - `data` (array of TagLink, required) — Ordered by the tag's normalized name. At most 500. - `id` (string (uuid), required) — The tag's id — what DELETE …/tags/{tag_id} and `{ id }` on POST …/tags take. - `name` (string, required) — The tag as first typed. Tags are matched by a normalized form: case, extra spaces and invisible characters are ignored. - `scope` ("org" | "team", required) — `org`: the whole org's. `team`: private to one team (an OAuth token sees it only when its person is in the team or an org admin). - `team` (object or null, required) — The owning team, for a team tag; null for an org tag. - `id` (string (uuid), required) - `name` (string, required) - `usage_count` (integer, required) — How many assets and projects carry the tag, across the org. - `source` ("user" | "generated", required) — `user`: a person or an API credential added it. `generated`: applied from a generated summary. - `added_at` (string (date-time), required) - `next_cursor` (string or null, required) — Always null: the list is returned whole (at most 500). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may see the project but lacks update on the project. - 404 (Problem) — not-found. - 422 (Problem) — validation (`tags`: 1–50 items, each `{ name }` or `{ id }`; a name that is empty or over 64 characters), or unknown-tag. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/projects/{id}/tags/{tag_id} Remove a tag from a project Takes the tag off the project; the tag stays in the org's list. 404 when the project does not carry it, so a second DELETE is a 404. Scopes: `projects:write` Parameters: - `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects. - `tag_id` (path, string (uuid), required) — The tag's id, from GET …/tags. Other responses: - 204 — Removed. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person lacks update on the project. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Uploads #### POST /api/v1/uploads Presign a single PUT, or start a multipart upload, for a file (step 1 of creating an asset with a file) PUT the file's bytes to `upload_url` with `headers`, then create the asset with `file.upload_id` (or attach it to an existing asset with POST /assets/{id}/files). A single PUT carries up to 5 GiB. With `multipart: true` (required above 5 GiB, allowed from 5 MiB, up to 5 TiB) the answer has `method: MULTIPART`, no `upload_url` or `headers`, and `multipart`: the server-chosen `part_size_bytes` and `part_count`. Sign the parts' URLs with POST /uploads/{id}/parts, PUT each part, then POST /uploads/{id}/complete. Scopes: `assets:write` Parameters: - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CreateUploadRequest` - `filename` (string, required) — The file's name; its extension is what slots match on. - `content_type` (string, required) — The MIME type the PUT will send. - `size_bytes` (integer, required) — The exact size of the file. Required: a single PUT's URL is bound to it, and a multipart upload is checked against it when completed. At most 5 GiB for a single PUT, 5 TiB with `multipart`. - `multipart` (boolean) — Upload in parts (required above 5 GiB, allowed from 5 MiB). The response's `multipart` gives the part size and count; sign the parts' URLs with POST /api/v1/uploads/{id}/parts, PUT each part, then POST /api/v1/uploads/{id}/complete. Default false: one PUT. Response 201 — `Upload` - `upload_id` (string (uuid), required) - `upload_url` (string or null, required) — PUT the file's bytes here, with `headers`. Null for a multipart upload: its parts' URLs come from `multipart.parts_url`. - `method` ("PUT" | "MULTIPART", required) — PUT: one PUT to upload_url. MULTIPART: parts, see `multipart`. - `headers` (map of string or null, required) — Headers the PUT must send. Null for a multipart upload (a part's PUT needs none). - `url_expires_at` (string (date-time) or null, required) — When upload_url stops accepting a new PUT (15 minutes). Null for a multipart upload: each part URL carries its own expiry. - `expires_at` (string (date-time), required) — When the upload must be used by (24 hours). - `max_bytes` (integer, required) - `status` ("pending", required) - `multipart` (UploadMultipart or null, required) — Set for a multipart upload; null for a single PUT. - `part_size_bytes` (integer, required) — Every part is exactly this size except the last, which is the remainder. Chosen by the server: at least 8 MiB, a whole number of MiB, at most 10 000 parts. - `part_count` (integer, required) — How many parts: ceil(size_bytes / part_size_bytes). Part numbers run 1..part_count; part n is bytes (n-1)·part_size_bytes onward. - `parts_url` (string, required) — Where to sign the parts' upload URLs (POST, `{ part_numbers }`). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 409 (Problem) — idempotency-in-flight. - 422 (Problem) — validation (e.g. size_bytes over 5 GiB without `multipart`, `multipart` under 5 MiB), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — storage-unavailable. #### GET /api/v1/uploads/{id} Get an upload's status: pending, landed, consumed or expired Scopes: `assets:write` Parameters: - `id` (path, string (uuid), required) — upload_id from POST /uploads. Response 200 — `UploadStatus` - `upload_id` (string (uuid), required) - `status` ("pending" | "landed" | "consumed" | "expired", required) — pending: no bytes yet (for a multipart upload: not completed yet). landed: the bytes are in storage, ready for POST /api/v1/assets or POST /api/v1/assets/{id}/files. consumed: an asset was created from it, or it was attached to one (asset_id). expired: past expires_at, or aborted. - `size_bytes` (integer or null, required) - `asset_id` (string (uuid) or null, required) — Set when consumed. - `expires_at` (string (date-time), required) - `multipart` (UploadMultipart or null, required) — Set for a multipart upload; null for a single PUT. - `part_size_bytes` (integer, required) — Every part is exactly this size except the last, which is the remainder. Chosen by the server: at least 8 MiB, a whole number of MiB, at most 10 000 parts. - `part_count` (integer, required) — How many parts: ceil(size_bytes / part_size_bytes). Part numbers run 1..part_count; part n is bytes (n-1)·part_size_bytes onward. - `parts_url` (string, required) — Where to sign the parts' upload URLs (POST, `{ part_numbers }`). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — storage-unavailable. #### POST /api/v1/uploads/{id}/parts Sign upload URLs for parts of a multipart upload Up to 100 distinct part numbers per request, each from 1 to `multipart.part_count`. PUT part n's bytes (offset (n-1)·`part_size_bytes`, `part_size_bytes` long, the last part the rest) to its URL within 15 minutes and keep the PUT's `ETag` header for …/complete. Signing writes nothing, so asking again just signs fresh URLs: no Idempotency-Key. Scopes: `assets:write` Parameters: - `id` (path, string (uuid), required) — upload_id from POST /uploads with `multipart: true`. Request body — `SignPartsRequest` - `part_numbers` (array of integer, required) — The parts to sign: 1 to 100 distinct part numbers, each from 1 to the upload's `multipart.part_count`. Sign in batches for more. Response 200 — `UploadPartUrlList` - `data` (array of UploadPartUrl, required) — One per requested part number, in the order asked. - `part_number` (integer, required) - `url` (string, required) — PUT exactly this part's bytes here, with no extra headers; keep the response's ETag header for POST …/complete. - `expires_at` (string (date-time), required) — When url stops accepting a new PUT (15 minutes). Sign the part again for a fresh one. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found — not this credential's upload, or no longer in progress (completed, aborted, used or expired). - 422 (Problem) — validation (part_numbers), or not-multipart — the upload is a single PUT. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — storage-unavailable. #### POST /api/v1/uploads/{id}/complete Complete a multipart upload from its parts Every part once, in any order, each with the ETag its PUT answered. The assembled file must be `size_bytes` long: otherwise it is discarded, the upload fails, and the answer is 422 `size-mismatch`. On success the upload is `landed`, ready for POST /assets or POST /assets/{id}/files. Completing an upload already completed answers its status again. Scopes: `assets:write` Parameters: - `id` (path, string (uuid), required) — upload_id from POST /uploads with `multipart: true`. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CompleteUploadRequest` - `parts` (array of object, required) — Every part, once each: all `multipart.part_count` of them, in any order. - `part_number` (integer, required) - `etag` (string, required) — The ETag header the part's PUT answered with, quotes included or not. Response 200 — `UploadStatus` - `upload_id` (string (uuid), required) - `status` ("pending" | "landed" | "consumed" | "expired", required) — pending: no bytes yet (for a multipart upload: not completed yet). landed: the bytes are in storage, ready for POST /api/v1/assets or POST /api/v1/assets/{id}/files. consumed: an asset was created from it, or it was attached to one (asset_id). expired: past expires_at, or aborted. - `size_bytes` (integer or null, required) - `asset_id` (string (uuid) or null, required) — Set when consumed. - `expires_at` (string (date-time), required) - `multipart` (UploadMultipart or null, required) — Set for a multipart upload; null for a single PUT. - `part_size_bytes` (integer, required) — Every part is exactly this size except the last, which is the remainder. Chosen by the server: at least 8 MiB, a whole number of MiB, at most 10 000 parts. - `part_count` (integer, required) — How many parts: ceil(size_bytes / part_size_bytes). Part numbers run 1..part_count; part n is bytes (n-1)·part_size_bytes onward. - `parts_url` (string, required) — Where to sign the parts' upload URLs (POST, `{ part_numbers }`). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found — not this credential's upload, or aborted, failed or expired. - 409 (Problem) — idempotency-in-flight. - 422 (Problem) — validation (parts — missing, repeated, or refused by storage), not-multipart, size-mismatch, or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — storage-unavailable. #### POST /api/v1/uploads/{id}/abort Abort a multipart upload in progress Frees the parts stored so far; the upload reads `expired`. Aborting again, or after completing, is 404. Scopes: `assets:write` Parameters: - `id` (path, string (uuid), required) — upload_id from POST /uploads with `multipart: true`. Other responses: - 204 — Aborted. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found — not this credential's upload, or not in progress (completed, aborted, used). - 422 (Problem) — not-multipart — the upload is a single PUT. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — storage-unavailable. ### Assets #### GET /api/v1/assets List and search the org's assets Every current asset in the org, whatever project it is in. Filters combine with AND. Keyset-paginated; no total. To poll for changes, pass `updated_after` with the newest `updated_at` you have seen and `sort=-updated_at` (or `updated_at`). Scopes: `assets:read` Parameters: - `q` (query, string) — Case-insensitive substring of the asset's name. Characters are matched literally. - `type_id` (query, string (uuid)) — Only assets of this type (GET /api/v1/asset-types). - `project_id` (query, string (uuid)) — Only assets placed in this project (GET /api/v1/projects). Not its sub-projects. - `external_ref` (query, string) — Only assets carrying exactly this external_ref. - `created_after` (query, string (date-time)) — Only assets created strictly after this instant (ISO 8601 with offset). - `created_before` (query, string (date-time)) — Only assets created strictly before this instant (ISO 8601 with offset). - `updated_after` (query, string (date-time)) — Only assets changed strictly after this instant (ISO 8601 with offset), compared to the millisecond — poll with the newest `updated_at` you have seen (that asset does not come back), sorted `-updated_at` or `updated_at`. - `category_id` (query, string (uuid)) — Only assets assigned this org category directly (a category an asset inherits from its project does not count). - `media_type` (query, "video" | "audio" | "image" | "document" | "other") — Only assets with a current file of this media type in one of their slots (derived files do not count). `other` is any slotted file that is not video, audio, image or document — subtitles, for one. - `tag` (query, array of string) — Only assets carrying a tag of this name — exact, ignoring case and extra spaces. Repeat the parameter (up to 10 times) for assets carrying every one. - `sort` (query, "created_at" | "-created_at" | "name" | "-name" | "updated_at" | "-updated_at") — Sort key; a leading `-` is descending. Default `-created_at` (newest first). Ties break on id. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `AssetSummaryList` - `data` (array of AssetSummary, required) - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `project` (ProjectRef, required) - `id` (string (uuid), required) - `name` (string, required) - `external_ref` (string or null, required) — The caller's own id for this asset, as given at creation. - `lifecycle_phase` ("draft" | "ingested" | "verified" | "delivered", required) - `edit_seq` (integer, required) — Increments on every change to name, fields, type or project. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad filter (`type_id`, `project_id`, `category_id`, `media_type`, `tag`, a timestamp), `sort`, `limit` or `cursor` (a cursor replayed with different parameters). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/assets Create an asset in a project, optionally from a landed upload With `file`, the upload becomes the asset's first file and the slot's pipeline starts exactly as for a browser upload; `runs` shows what had started by the time of the response. Scopes: `assets:write` Parameters: - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CreateAssetRequest` - `type_id` (string (uuid), required) — An asset type from GET /api/v1/asset-types. - `project_id` (string (uuid), required) — The project the asset is placed in, from GET /api/v1/projects. Required: an API caller always names its project. - `name` (string, required) - `fields` (object (any keys)) — Field values keyed by field key, validated against the asset type. - `external_ref` (string) — Your own id for this asset; returned on every read. - `file` (object) — Attach an uploaded file. Its slot's pipeline (proxy, captions, QC…) starts as for a browser upload. - `upload_id` (string (uuid), required) — A landed upload this key created with POST /api/v1/uploads. - `slot_id` (string (uuid)) — The file slot (GET /api/v1/asset-types). Omit to use the type's one slot that matches the file's media type. Response 201 — `Asset` - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `project` (ProjectRef, required) - `id` (string (uuid), required) - `name` (string, required) - `fields` (object (any keys), required) — Field values keyed by field key, as defined by the asset type. - `external_ref` (string or null, required) — The caller's own id for this asset, as given at creation. - `lifecycle_phase` ("draft" | "ingested" | "verified" | "delivered", required) - `edit_seq` (integer, required) — Increments on every change to name, fields, type or project. Also the ETag. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `files` (array of AssetFile, required) - `id` (string (uuid), required) - `slot` (object or null, required) — The slot a caller's file was attached to; null for a file the pipeline derived. - `id` (string (uuid), required) - `name` (string or null, required) - `role` (string or null, required) — null for a file attached to a slot; for a derived file, what the pipeline made: proxy, thumbnail, captions, conformed (more may be added). - `media_type` (string, required) - `mime_type` (string or null, required) - `extension` (string or null, required) - `size_bytes` (integer or null, required) - `version` (integer, required) - `uploaded_at` (string (date-time), required) - `root_file_id` (string (uuid), required) — The file's version chain: the id of its first version. Every version of the file shares it; GET /assets/{id}/files/{file_id} lists the chain. - `version_note` (string or null, required) — Why this version was added, as its uploader wrote it; null when none was given. - `uploaded_by` (object or null, required) — The person who added this version ({ id: their principal id, name }). Null for a file the pipeline derived and for one added with an API key (every key of an org acts as one service principal, so the key is not recoverable from the file). - `id` (string (uuid), required) - `name` (string, required) - `runs` (array of Run, required) — The newest runs started against this asset, newest first, at most 10. - `id` (string (uuid), required) - `workflow` (object, required) — The template version the run is pinned to (any version's id names its template in GET /api/v1/workflows/{id}). `name` is null when the run is `private`. - `id` (string (uuid), required) - `name` (string or null, required) - `asset` (object, required) — The asset the run is about. `name` is null for an OAuth token whose person can no longer read the asset (a run they started). - `id` (string (uuid), required) - `name` (string or null, required) - `status` ("active" | "completed" | "cancelled" | "failed", required) - `private` (boolean, required) — True when the run's template is private to a team the token's person is not in: the run's existence, status, team and times are shown, its workflow name, steps, current step, incident and params are not (always false for an API key). - `team` (object or null, required) — The team that owns the run, snapshotted from its template at start; null for an unowned run. - `id` (string (uuid), required) - `name` (string, required) - `parent_run_id` (string (uuid) or null, required) — For a sub-workflow run, the run that started it. - `started_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves on every change of state — poll on it with GET /api/v1/runs?updated_after=. - `completed_at` (string (date-time) or null, required) - `started_via` (string, required) — api (started through this API) | manual (in the app) | trigger (by an arrival trigger, e.g. an upload). More may be added. - `current_step` (object or null, required) — The first open step at the run's current gate, while the run is active; null otherwise. - `id` (string (uuid), required) - `name` (string, required) - `status` (string, required) - `awaiting` ("approval" | "completion" | "automation" or null, required) — What the step waits on: `approval` (a person's verdict), `completion` (a person marking it done), `automation` (the pipeline — an automated step, a gate or a sub-workflow); null while it is parked on an incident. - `steps` (array of RunStep, required) - `id` (string (uuid), required) — The template step's id (of the template version the run is pinned to). - `name` (string, required) - `status` (string, required) — pending | in_progress | submitted | approved | completed | failed | skipped | incident (more may be added). - `incident` (object or null, required) — Set while an active run is parked on a step that failed and waits for an operator to retry it or fail the run (in the app). - `message` (string, required) - `params` (object (any keys) or null, required) — The run's launch parameters, as validated against the template's param_schema. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 409 (Problem) — idempotency-in-flight. - 422 (Problem) — validation (type_id, project_id, fields, file), upload-not-landed, or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — storage-unavailable. #### GET /api/v1/assets/{id} Get an asset with its current files and recent runs Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `Asset` - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `project` (ProjectRef, required) - `id` (string (uuid), required) - `name` (string, required) - `fields` (object (any keys), required) — Field values keyed by field key, as defined by the asset type. - `external_ref` (string or null, required) — The caller's own id for this asset, as given at creation. - `lifecycle_phase` ("draft" | "ingested" | "verified" | "delivered", required) - `edit_seq` (integer, required) — Increments on every change to name, fields, type or project. Also the ETag. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `files` (array of AssetFile, required) - `id` (string (uuid), required) - `slot` (object or null, required) — The slot a caller's file was attached to; null for a file the pipeline derived. - `id` (string (uuid), required) - `name` (string or null, required) - `role` (string or null, required) — null for a file attached to a slot; for a derived file, what the pipeline made: proxy, thumbnail, captions, conformed (more may be added). - `media_type` (string, required) - `mime_type` (string or null, required) - `extension` (string or null, required) - `size_bytes` (integer or null, required) - `version` (integer, required) - `uploaded_at` (string (date-time), required) - `root_file_id` (string (uuid), required) — The file's version chain: the id of its first version. Every version of the file shares it; GET /assets/{id}/files/{file_id} lists the chain. - `version_note` (string or null, required) — Why this version was added, as its uploader wrote it; null when none was given. - `uploaded_by` (object or null, required) — The person who added this version ({ id: their principal id, name }). Null for a file the pipeline derived and for one added with an API key (every key of an org acts as one service principal, so the key is not recoverable from the file). - `id` (string (uuid), required) - `name` (string, required) - `runs` (array of Run, required) — The newest runs started against this asset, newest first, at most 10. - `id` (string (uuid), required) - `workflow` (object, required) — The template version the run is pinned to (any version's id names its template in GET /api/v1/workflows/{id}). `name` is null when the run is `private`. - `id` (string (uuid), required) - `name` (string or null, required) - `asset` (object, required) — The asset the run is about. `name` is null for an OAuth token whose person can no longer read the asset (a run they started). - `id` (string (uuid), required) - `name` (string or null, required) - `status` ("active" | "completed" | "cancelled" | "failed", required) - `private` (boolean, required) — True when the run's template is private to a team the token's person is not in: the run's existence, status, team and times are shown, its workflow name, steps, current step, incident and params are not (always false for an API key). - `team` (object or null, required) — The team that owns the run, snapshotted from its template at start; null for an unowned run. - `id` (string (uuid), required) - `name` (string, required) - `parent_run_id` (string (uuid) or null, required) — For a sub-workflow run, the run that started it. - `started_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves on every change of state — poll on it with GET /api/v1/runs?updated_after=. - `completed_at` (string (date-time) or null, required) - `started_via` (string, required) — api (started through this API) | manual (in the app) | trigger (by an arrival trigger, e.g. an upload). More may be added. - `current_step` (object or null, required) — The first open step at the run's current gate, while the run is active; null otherwise. - `id` (string (uuid), required) - `name` (string, required) - `status` (string, required) - `awaiting` ("approval" | "completion" | "automation" or null, required) — What the step waits on: `approval` (a person's verdict), `completion` (a person marking it done), `automation` (the pipeline — an automated step, a gate or a sub-workflow); null while it is parked on an incident. - `steps` (array of RunStep, required) - `id` (string (uuid), required) — The template step's id (of the template version the run is pinned to). - `name` (string, required) - `status` (string, required) — pending | in_progress | submitted | approved | completed | failed | skipped | incident (more may be added). - `incident` (object or null, required) — Set while an active run is parked on a step that failed and waits for an operator to retry it or fail the run (in the app). - `message` (string, required) - `params` (object (any keys) or null, required) — The run's launch parameters, as validated against the template's param_schema. Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### PATCH /api/v1/assets/{id} Update an asset's name, fields, project or external_ref If-Match is required: send the ETag from the last GET (or the bare `edit_seq`). `fields` replaces the whole bag. A `project_id` change moves the asset; filing it into a project that is not open revokes its share links. Scopes: `assets:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `If-Match` (header, string, required) — The asset's ETag, or its bare edit_seq. Request body — `UpdateAssetRequest`: At least one of name, fields, project_id, external_ref. - `name` (string) - `fields` (object (any keys)) — The asset's whole field bag, validated against its type: keys left out are removed (send back what GET returned, changed). Legacy `__legacy` values are carried forward unless sent as null. - `project_id` (string (uuid)) — Move the asset to this project (GET /api/v1/projects). - `external_ref` (string or null) — Your own id for this asset; null clears it. - `schema_version` (integer) — The asset type's schema_version your `fields` were built against. When they fail validation and the type has moved past it, the answer is 409 type-changed instead of 422. Response 200 — `Asset` - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `project` (ProjectRef, required) - `id` (string (uuid), required) - `name` (string, required) - `fields` (object (any keys), required) — Field values keyed by field key, as defined by the asset type. - `external_ref` (string or null, required) — The caller's own id for this asset, as given at creation. - `lifecycle_phase` ("draft" | "ingested" | "verified" | "delivered", required) - `edit_seq` (integer, required) — Increments on every change to name, fields, type or project. Also the ETag. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `files` (array of AssetFile, required) - `id` (string (uuid), required) - `slot` (object or null, required) — The slot a caller's file was attached to; null for a file the pipeline derived. - `id` (string (uuid), required) - `name` (string or null, required) - `role` (string or null, required) — null for a file attached to a slot; for a derived file, what the pipeline made: proxy, thumbnail, captions, conformed (more may be added). - `media_type` (string, required) - `mime_type` (string or null, required) - `extension` (string or null, required) - `size_bytes` (integer or null, required) - `version` (integer, required) - `uploaded_at` (string (date-time), required) - `root_file_id` (string (uuid), required) — The file's version chain: the id of its first version. Every version of the file shares it; GET /assets/{id}/files/{file_id} lists the chain. - `version_note` (string or null, required) — Why this version was added, as its uploader wrote it; null when none was given. - `uploaded_by` (object or null, required) — The person who added this version ({ id: their principal id, name }). Null for a file the pipeline derived and for one added with an API key (every key of an org acts as one service principal, so the key is not recoverable from the file). - `id` (string (uuid), required) - `name` (string, required) - `runs` (array of Run, required) — The newest runs started against this asset, newest first, at most 10. - `id` (string (uuid), required) - `workflow` (object, required) — The template version the run is pinned to (any version's id names its template in GET /api/v1/workflows/{id}). `name` is null when the run is `private`. - `id` (string (uuid), required) - `name` (string or null, required) - `asset` (object, required) — The asset the run is about. `name` is null for an OAuth token whose person can no longer read the asset (a run they started). - `id` (string (uuid), required) - `name` (string or null, required) - `status` ("active" | "completed" | "cancelled" | "failed", required) - `private` (boolean, required) — True when the run's template is private to a team the token's person is not in: the run's existence, status, team and times are shown, its workflow name, steps, current step, incident and params are not (always false for an API key). - `team` (object or null, required) — The team that owns the run, snapshotted from its template at start; null for an unowned run. - `id` (string (uuid), required) - `name` (string, required) - `parent_run_id` (string (uuid) or null, required) — For a sub-workflow run, the run that started it. - `started_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves on every change of state — poll on it with GET /api/v1/runs?updated_after=. - `completed_at` (string (date-time) or null, required) - `started_via` (string, required) — api (started through this API) | manual (in the app) | trigger (by an arrival trigger, e.g. an upload). More may be added. - `current_step` (object or null, required) — The first open step at the run's current gate, while the run is active; null otherwise. - `id` (string (uuid), required) - `name` (string, required) - `status` (string, required) - `awaiting` ("approval" | "completion" | "automation" or null, required) — What the step waits on: `approval` (a person's verdict), `completion` (a person marking it done), `automation` (the pipeline — an automated step, a gate or a sub-workflow); null while it is parked on an incident. - `steps` (array of RunStep, required) - `id` (string (uuid), required) — The template step's id (of the template version the run is pinned to). - `name` (string, required) - `status` (string, required) — pending | in_progress | submitted | approved | completed | failed | skipped | incident (more may be added). - `incident` (object or null, required) — Set while an active run is parked on a step that failed and waits for an operator to retry it or fail the run (in the app). - `message` (string, required) - `params` (object (any keys) or null, required) — The run's launch parameters, as validated against the template's param_schema. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 409 (Problem) — type-changed — the type's schema moved past `schema_version` and the fields no longer validate. - 412 (Problem) — precondition-failed — If-Match is stale; `edit_seq` in the body is the current one. - 422 (Problem) — validation (empty body, name, fields, project_id, external_ref). - 428 (Problem) — precondition-required — no If-Match. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/assets/{id} Delete an asset (every version) Soft-deletes the asset; it no longer appears in reads or lists. No If-Match: deleting is idempotent by outcome, so a second DELETE is a 404. An asset under legal hold is refused with 409 `legal-hold`, whose detail gives the hold's reason. Scopes: `assets:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). Other responses: - 204 — Deleted. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 409 (Problem) — legal-hold — the asset is under legal hold. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/assets/{id}/referenced-by List the records that reference an asset Every live record whose reference field points at the asset (any of its versions), as `{ record, field_key, field_name }` — a record pointing at it through two fields appears twice — ordered by the record's title. For an OAuth token, records its person may not read and fields they may not read are left out, so a page may be short and still have a next one. 404 when the org does not have custom records, and for an asset the credential cannot read. Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page. Response 200 — `ReferencingRecordList` - `data` (array of ReferencingRecord, required) - `record` (object, required) - `id` (string (uuid), required) - `name` (string, required) — The record's title. - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `field_key` (string, required) — The record's reference field that points at the asset. - `field_name` (string, required) — That field's label, as the app shows it. - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. A page may hold fewer than `limit` entries (those the credential may not see are left out) and still have a next page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/assets/{id}/lineage Get an asset's derivation lineage What the asset derives from (`ancestors`) and what derives from it (`descendants`) — proxies, clips promoted from it, deliveries — over the lineage graph, one node per asset at its smallest depth, at most 64 steps each way, returned whole. Where an edge joins another organisation (a delivery to or from it) the walk stops and the node is a **boundary**: `asset: null`, `restricted: true`, `boundary: { org_name, role, receipt_id }` — nothing of the other org's asset or graph. For an OAuth token, an asset in the graph its person may not read keeps its place, depth and type with `asset: null` and `restricted: true`. 404 for an asset the credential cannot read. Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). Response 200 — `Lineage`: An asset's derivation lineage: ancestors and descendants over the lineage graph, at most 64 steps each way, cycle-guarded. Returned whole. - `root_id` (string (uuid), required) — The asset's root id (its first version's id). - `self` (LineageNode, required) — The asset asked about, at depth 0. - `asset` (object or null, required) — The asset (its current version's id and name). Null for a boundary node, and — for an OAuth token — for an asset its person may not read (`restricted`). - `id` (string (uuid), required) - `name` (string, required) - `version` (integer or null, required) — The asset's current version number; null for a boundary node. - `type` (object or null, required) — The asset's type — kept on a restricted node (the envelope); null for a boundary node or an untyped asset. - `id` (string (uuid), required) - `name` (string, required) - `depth` (integer, required) — Steps from the asset asked about: 0 for `self`, 1 for a direct parent or child. - `relationship` (string or null, required) — The edge's role (`derivation`, `delivered`…) by which the node was reached at its smallest depth; null for `self`. - `restricted` (boolean, required) — True when the node's content is withheld: a boundary node, or an asset an OAuth token's person may not read. The node keeps its place so the chain has no hole. - `boundary` (LineageBoundary or null, required) — Set when the edge here joins another org; the walk stops at it. - `org_name` (string or null, required) — The other organisation's name. - `role` (string, required) — The edge's relationship, e.g. `delivered`. - `receipt_id` (string (uuid) or null, required) — The delivery receipt that wrote the edge, when there is one. - `ancestors` (array of LineageNode, required) — What it derives from, nearest first (one node per asset, at its smallest depth), then any boundary met. - `asset` (object or null, required) — The asset (its current version's id and name). Null for a boundary node, and — for an OAuth token — for an asset its person may not read (`restricted`). - `id` (string (uuid), required) - `name` (string, required) - `version` (integer or null, required) — The asset's current version number; null for a boundary node. - `type` (object or null, required) — The asset's type — kept on a restricted node (the envelope); null for a boundary node or an untyped asset. - `id` (string (uuid), required) - `name` (string, required) - `depth` (integer, required) — Steps from the asset asked about: 0 for `self`, 1 for a direct parent or child. - `relationship` (string or null, required) — The edge's role (`derivation`, `delivered`…) by which the node was reached at its smallest depth; null for `self`. - `restricted` (boolean, required) — True when the node's content is withheld: a boundary node, or an asset an OAuth token's person may not read. The node keeps its place so the chain has no hole. - `boundary` (LineageBoundary or null, required) — Set when the edge here joins another org; the walk stops at it. - `org_name` (string or null, required) — The other organisation's name. - `role` (string, required) — The edge's relationship, e.g. `delivered`. - `receipt_id` (string (uuid) or null, required) — The delivery receipt that wrote the edge, when there is one. - `descendants` (array of LineageNode, required) — What derives from it, nearest first, likewise. - `asset` (object or null, required) — The asset (its current version's id and name). Null for a boundary node, and — for an OAuth token — for an asset its person may not read (`restricted`). - `id` (string (uuid), required) - `name` (string, required) - `version` (integer or null, required) — The asset's current version number; null for a boundary node. - `type` (object or null, required) — The asset's type — kept on a restricted node (the envelope); null for a boundary node or an untyped asset. - `id` (string (uuid), required) - `name` (string, required) - `depth` (integer, required) — Steps from the asset asked about: 0 for `self`, 1 for a direct parent or child. - `relationship` (string or null, required) — The edge's role (`derivation`, `delivered`…) by which the node was reached at its smallest depth; null for `self`. - `restricted` (boolean, required) — True when the node's content is withheld: a boundary node, or an asset an OAuth token's person may not read. The node keeps its place so the chain has no hole. - `boundary` (LineageBoundary or null, required) — Set when the edge here joins another org; the walk stops at it. - `org_name` (string or null, required) — The other organisation's name. - `role` (string, required) — The edge's relationship, e.g. `delivered`. - `receipt_id` (string (uuid) or null, required) — The delivery receipt that wrote the edge, when there is one. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/assets/{id}/playback Get an asset's playback descriptor What a player needs to play the asset's primary video: `status` (`ready`, `processing` or `none`), the HLS manifest URL (`hls_url`, carrying its signed access token when the delivery is signed), the poster, the WebVTT caption tracks and the progressive MP4 proxies. Every URL is short-lived: `expires_at` is the earliest moment one may stop working, so fetch the descriptor again rather than caching a URL. `processing` carries no URL; `none` means there is no HLS ladder to play (no video, a failed or missing transcode, or a delivery the API cannot play yet) — proxies, captions and poster are still listed where they exist. The original upload is never served here: playing is a read, the original is an export (`GET /assets/{id}/files/{file_id}/download`). 404 for an asset the credential cannot read (for an OAuth token, read on the asset). Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). Response 200 — `Playback`: What a player needs to play an asset's primary video: the HLS manifest, poster, caption tracks and proxies — every URL short-lived, never the original (the master is an export, not a read). - `status` ("ready" | "processing" | "none", required) — `ready`: the HLS ladder is packaged and `hls_url` plays it. `processing`: the ladder is being made — nothing else in the descriptor is set; ask again. `none`: there is no ladder to play — the asset has no video, its transcode failed or was never started, or its delivery cannot be played through the API yet. - `hls_url` (string or null, required) — The HLS manifest (with its signed access token when the delivery is signed); set only when `status` is `ready`. Do not store it: fetch the descriptor again for a fresh one. - `expires_at` (string (date-time) or null, required) — The earliest moment a URL in this descriptor may stop working; null when it carries no URL. Re-fetch the descriptor before then rather than caching URLs. - `poster_url` (string or null, required) — A presigned GET for the asset's poster still, when it has one. - `duration_seconds` (number or null, required) — The played file's measured duration; null until measured or when there is no video. - `captions` (array of PlaybackCaption, required) — Caption tracks, originals before machine translations, oldest first — the first is the default track. - `language` (string or null, required) — The track's BCP 47 language tag, or null when it is not recorded. - `label` (string, required) — A label for a track menu: the language's own name, plus " (auto-translated)" or " (auto-generated)" for a machine-made track; else the slot's name or `Subtitles`. - `url` (string, required) — A presigned GET for the WebVTT file. Stops working at the descriptor's `expires_at`. - `kind` ("subtitles", required) — The `` to use. Always `subtitles` today. - `proxies` (array of PlaybackProxy, required) — Progressive MP4 proxies derived from the played file, tallest first. - `variant` (string, required) — The rendition's variant, e.g. `1080p`, `720p`. - `url` (string, required) — A presigned GET for the MP4. Stops working at the descriptor's `expires_at`. - `width` (integer or null, required) — Pixels, as measured; null until measured. - `height` (integer or null, required) — Pixels, as measured; null until measured. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — storage-unavailable — the URLs or the delivery's access token could not be signed; retry. #### GET /api/v1/assets/{id}/credits List the people credited on an asset The asset's credits (credits belong to the asset, not a version): `{ id, person, role, billing_order }`, by billing order (unset last), then name. Returned whole, at most 500. Archived people are left out. Read only: credits are given in the app. 404 for an asset the credential cannot read (for an OAuth token, read on the asset). 404 when the org does not have people. Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). Response 200 — `AssetCreditList` - `data` (array of AssetCredit, required) — By billing_order (unset last), then the person's name. Returned whole, at most 500. - `id` (string (uuid), required) — The credit's id. - `person` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `role` (string or null, required) — The role credited (`Director`, `Host`…), as entered in the app; null when none was given. - `billing_order` (integer or null, required) — Billing position, lowest first; null when unset. - `next_cursor` (string or null, required) — Always null: the list is returned whole. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/assets/{id}/categories List an asset's categories The categories assigned to the asset's current version, each with its group, returned whole — the assignments `GET /assets?category_id=` matches on (a category the asset's project carries is not listed). Read only: categories are assigned in the app. 404 for an asset the credential cannot read (for an OAuth token, read on the asset). 404 when the org does not have categories. Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). Response 200 — `AssetCategoryList` - `data` (array of AssetCategory, required) — By group name, then name. Returned whole. - `id` (string (uuid), required) — The category's id (GET /api/v1/categories). - `name` (string, required) - `group` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `next_cursor` (string or null, required) — Always null: the list is returned whole. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Asset files #### GET /api/v1/assets/{id}/files List an asset's current files with their technical metadata Every current file of the asset — the ones attached to a slot, then the ones the pipeline derived (proxy, thumbnail, captions…) — in the order `Asset.files` uses, each with its version chain (`root_file_id`), who added it, its version note and `technical`: what the pipeline measured (duration, dimensions, codec, loudness), null until the measure pass has run. A derived file's `size_bytes` may be null. Returned whole: `next_cursor` is always null. Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). Response 200 — `AssetFileList` - `data` (array of AssetFileDetail, required) - `id` (string (uuid), required) - `slot` (object or null, required) — The slot a caller's file was attached to; null for a file the pipeline derived. - `id` (string (uuid), required) - `name` (string or null, required) - `role` (string or null, required) — null for a file attached to a slot; for a derived file, what the pipeline made: proxy, thumbnail, captions, conformed (more may be added). - `media_type` (string, required) - `mime_type` (string or null, required) - `extension` (string or null, required) - `size_bytes` (integer or null, required) - `version` (integer, required) - `uploaded_at` (string (date-time), required) - `root_file_id` (string (uuid), required) — The file's version chain: the id of its first version. Every version of the file shares it; GET /assets/{id}/files/{file_id} lists the chain. - `version_note` (string or null, required) — Why this version was added, as its uploader wrote it; null when none was given. - `uploaded_by` (object or null, required) — The person who added this version ({ id: their principal id, name }). Null for a file the pipeline derived and for one added with an API key (every key of an org acts as one service principal, so the key is not recoverable from the file). - `id` (string (uuid), required) - `name` (string, required) - `technical` (VideoInfo or AudioInfo or ImageInfo or null, required) — What the pipeline measured, by `media_type`: VideoInfo for video, AudioInfo for audio, ImageInfo for image. Null until the measure pass has run on the file, and always null for other media types (documents, subtitles). - `duration_seconds` (number or null, required) - `width` (integer or null, required) - `height` (integer or null, required) - `codec` (string or null, required) — The video codec, as the probe names it (h264, prores, …). - `frame_rate` (number or null, required) - `audio_codec` (string or null, required) — The codec of the file's interleaved audio, if any. - `audio_sample_rate` (integer or null, required) - `integrated_loudness` (number or null, required) — Integrated loudness of the interleaved audio, LUFS (EBU R128). - `color_primaries` (string or null, required) - `color_transfer` (string or null, required) - `next_cursor` (string or null, required) — Always null: an asset's current files are few and are returned whole. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/assets/{id}/files Attach an uploaded file to an asset, or replace one of its files with a new version `upload_id` is a landed upload this credential made (POST /uploads, then PUT the bytes). With `replace_file_id` (a current file of the asset) the upload becomes the next version of that file's chain; the old version is kept and stays downloadable. Without it, a slot that holds one file gets a new version of that file, and a slot that holds several (`multiple`) gets a new file beside the others. The slot is `replace_file_id`'s, else `slot_id`, else the type's one slot matching the file's media type. The slot's pipeline (proxy, captions, QC…) starts as for a browser upload; GET the asset for its `runs`. An OAuth token's person needs edit on the asset. An asset under legal hold is refused with 409 `legal-hold`. Scopes: `assets:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `AttachFileRequest`: Attach an uploaded file to an asset: a new file in a slot, or a new version of one. - `upload_id` (string (uuid), required) — A landed upload this credential created with POST /api/v1/uploads. - `slot_id` (string (uuid)) — The file slot (GET /api/v1/asset-types). Omit to use the slot of `replace_file_id`, or else the type's one slot that matches the file's media type. - `replace_file_id` (string (uuid)) — A current file of this asset (an id from `files`) to replace: the upload becomes the next version of that file's chain, and the old version is kept. Without it, a slot that holds one file gets a new version of that file, and a slot that holds several (`multiple`) gets a new file beside the others. - `version_note` (string) — Why this version exists ("re-grade per client note"); shown in the file's history. Response 201 — `AssetFile`: A current file of the asset: one attached to a slot, or one the pipeline derived from it. GET /assets/{id}/files/{file_id}/download signs a URL for its bytes. - `id` (string (uuid), required) - `slot` (object or null, required) — The slot a caller's file was attached to; null for a file the pipeline derived. - `id` (string (uuid), required) - `name` (string or null, required) - `role` (string or null, required) — null for a file attached to a slot; for a derived file, what the pipeline made: proxy, thumbnail, captions, conformed (more may be added). - `media_type` (string, required) - `mime_type` (string or null, required) - `extension` (string or null, required) - `size_bytes` (integer or null, required) - `version` (integer, required) - `uploaded_at` (string (date-time), required) - `root_file_id` (string (uuid), required) — The file's version chain: the id of its first version. Every version of the file shares it; GET /assets/{id}/files/{file_id} lists the chain. - `version_note` (string or null, required) — Why this version was added, as its uploader wrote it; null when none was given. - `uploaded_by` (object or null, required) — The person who added this version ({ id: their principal id, name }). Null for a file the pipeline derived and for one added with an API key (every key of an org acts as one service principal, so the key is not recoverable from the file). - `id` (string (uuid), required) - `name` (string, required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may see the asset but not edit it. - 404 (Problem) — not-found. - 409 (Problem) — legal-hold — the asset is under legal hold; or idempotency-in-flight. - 422 (Problem) — validation (upload_id, slot_id, replace_file_id — not a current file of this asset, a derived file, or in another slot — version_note), upload-not-landed, or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — storage-unavailable. #### GET /api/v1/assets/{id}/files/{file_id} Get one file of an asset, any version, with its version chain By any version's id — current or not. `versions` lists the whole chain, newest first; download any of them with …/download. A file replaced (POST …/files with `replace_file_id`, or in the app) or restored (…/restore) is a new version in the same chain. Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `file_id` (path, string (uuid), required) — A file id from the asset's `files`, or any version's id from a file's `versions`. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `AssetFileVersions`: One file of an asset — any version — with its technical metadata and its whole version chain. - `id` (string (uuid), required) - `slot` (object or null, required) — The slot a caller's file was attached to; null for a file the pipeline derived. - `id` (string (uuid), required) - `name` (string or null, required) - `role` (string or null, required) — null for a file attached to a slot; for a derived file, what the pipeline made: proxy, thumbnail, captions, conformed (more may be added). - `media_type` (string, required) - `mime_type` (string or null, required) - `extension` (string or null, required) - `size_bytes` (integer or null, required) - `version` (integer, required) - `uploaded_at` (string (date-time), required) - `root_file_id` (string (uuid), required) — The file's version chain: the id of its first version. Every version of the file shares it; GET /assets/{id}/files/{file_id} lists the chain. - `version_note` (string or null, required) — Why this version was added, as its uploader wrote it; null when none was given. - `uploaded_by` (object or null, required) — The person who added this version ({ id: their principal id, name }). Null for a file the pipeline derived and for one added with an API key (every key of an org acts as one service principal, so the key is not recoverable from the file). - `id` (string (uuid), required) - `name` (string, required) - `technical` (VideoInfo or AudioInfo or ImageInfo or null, required) — What the pipeline measured, by `media_type`: VideoInfo for video, AudioInfo for audio, ImageInfo for image. Null until the measure pass has run on the file, and always null for other media types (documents, subtitles). - `duration_seconds` (number or null, required) - `width` (integer or null, required) - `height` (integer or null, required) - `codec` (string or null, required) — The video codec, as the probe names it (h264, prores, …). - `frame_rate` (number or null, required) - `audio_codec` (string or null, required) — The codec of the file's interleaved audio, if any. - `audio_sample_rate` (integer or null, required) - `integrated_loudness` (number or null, required) — Integrated loudness of the interleaved audio, LUFS (EBU R128). - `color_primaries` (string or null, required) - `color_transfer` (string or null, required) - `is_current` (boolean, required) — Whether this is the chain's current version. Any version's id can be read and downloaded. - `versions` (array of FileVersion, required) — Every version in the file's chain, newest first (current or not). Chains are short; returned whole. - `id` (string (uuid), required) - `version` (integer, required) - `is_current` (boolean, required) - `size_bytes` (integer or null, required) - `uploaded_at` (string (date-time), required) - `uploaded_by` (object or null, required) — As on AssetFile: the person, or null for the pipeline and for an API key. - `id` (string (uuid), required) - `name` (string, required) - `version_note` (string or null, required) Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/assets/{id}/files/{file_id}/restore Make an earlier version of a file current again `file_id` is an earlier version (an id from the file's `versions` whose `is_current` is false). Restoring appends: the chain gets a NEW version carrying that version's bytes, and every earlier row stays as it was. The body is optional; `version_note` defaults to "Restored v". No pipeline runs (the bytes were processed when they first arrived). A derived file cannot be restored. An OAuth token's person needs edit on the asset. An asset under legal hold is refused with 409 `legal-hold`. Scopes: `assets:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `file_id` (path, string (uuid), required) — A file id from the asset's `files`, or any version's id from a file's `versions`. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `RestoreFileRequest`: The body is optional; an empty body restores with the default note. - `version_note` (string) — Why the earlier version is coming back. Default: "Restored v". Response 201 — `AssetFile`: A current file of the asset: one attached to a slot, or one the pipeline derived from it. GET /assets/{id}/files/{file_id}/download signs a URL for its bytes. - `id` (string (uuid), required) - `slot` (object or null, required) — The slot a caller's file was attached to; null for a file the pipeline derived. - `id` (string (uuid), required) - `name` (string or null, required) - `role` (string or null, required) — null for a file attached to a slot; for a derived file, what the pipeline made: proxy, thumbnail, captions, conformed (more may be added). - `media_type` (string, required) - `mime_type` (string or null, required) - `extension` (string or null, required) - `size_bytes` (integer or null, required) - `version` (integer, required) - `uploaded_at` (string (date-time), required) - `root_file_id` (string (uuid), required) — The file's version chain: the id of its first version. Every version of the file shares it; GET /assets/{id}/files/{file_id} lists the chain. - `version_note` (string or null, required) — Why this version was added, as its uploader wrote it; null when none was given. - `uploaded_by` (object or null, required) — The person who added this version ({ id: their principal id, name }). Null for a file the pipeline derived and for one added with an API key (every key of an org acts as one service principal, so the key is not recoverable from the file). - `id` (string (uuid), required) - `name` (string, required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may see the asset but not edit it. - 404 (Problem) — not-found. - 409 (Problem) — legal-hold — the asset is under legal hold; or idempotency-in-flight. - 422 (Problem) — already-current — that version is the chain's current one; validation (a derived file, version_note); or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/assets/{id}/files/{file_id}/download Get a short-lived URL for one of an asset's files, any version A presigned GET for a file of the asset — slotted or derived, the current version or any earlier one (an id from `versions`) — valid for 5 minutes. A download is an export: an OAuth token's person needs export on the asset (else 404). A JSON body, not a redirect; fetch `url` directly (no Authorization header). A file with no bytes in storage yet is a 404. Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `file_id` (path, string (uuid), required) — A file id from the asset's `files`, or any version's id from a file's `versions`. Response 200 — `DownloadUrl`: A short-lived URL for one file's bytes. - `url` (string, required) — A presigned GET for the file's bytes. Do not store it: it stops working at expires_at. - `expires_at` (string (date-time), required) — When url stops working (5 minutes after issue). Ask again for a fresh one. - `filename` (string, required) — The stored object's file name. - `content_type` (string or null, required) - `size_bytes` (integer or null, required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 503 (Problem) — storage-unavailable. ### Asset comments and markers #### GET /api/v1/assets/{id}/comments List an asset's review comments Every comment on the asset (any of its versions — comments belong to the asset), flat: thread them by `parent_id`. In conversation order (`created_at`) unless `sort` says otherwise. `resolved=true|false` keeps only resolved or open comments (a reply on its own state). To poll, pass `updated_after` with the newest `updated_at` you have seen (compared to the millisecond, so that comment does not come back) and `sort=updated_at`; editing, resolving or reopening a comment moves its `updated_at`, and a deleted comment simply leaves the list. Written with POST (and PATCH / DELETE one) under `review:write`. 404 for an asset the credential cannot read (for an OAuth token, read on the asset). Scopes: `review:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `resolved` (query, "true" | "false") — `true`: only resolved comments; `false`: only open ones. Omitted: both. A reply is filtered on its own state, not its thread's. - `updated_after` (query, string (date-time)) — Only comments changed strictly after this instant (ISO 8601 with offset), compared to the millisecond — pass the newest `updated_at` you have seen and that comment is not returned again. - `sort` (query, "created_at" | "-created_at" | "updated_at" | "-updated_at") — Sort key; a leading `-` is descending. Default `created_at` (conversation order, as the app shows them). Ties break on id. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `CommentList` - `data` (array of Comment, required) - `id` (string (uuid), required) - `body` (string, required) - `author` (object or null, required) — Who wrote it: `id` is their principal in the org, `name` their display name. Null if the author's account is gone. A comment written with an org API key is authored by the org's API service principal, named `API`. - `id` (string (uuid), required) - `name` (string, required) - `timestamp_seconds` (number or null, required) — The point in the media the comment is pinned to, in seconds from the start; null for a comment on the asset as a whole. - `parent_id` (string (uuid) or null, required) — The comment this one replies to, or null for a top-level comment. The list is flat: thread it by `parent_id`. - `resolved_at` (string (date-time) or null, required) — When it was marked resolved, or null while open. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves when the body is edited or the comment is resolved or reopened. - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `resolved`, `updated_after`, `sort`, `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/assets/{id}/comments Comment on an asset, or reply to a comment Adds a review comment as the credential: an OAuth token writes as its person, who needs read on the asset (anyone who can see an asset may comment, as in the app); an API key writes as the org's API service principal, shown as `API`. `timestamp_seconds` pins it to a point in the media; `parent_id` makes it a reply to a comment on the same asset. Audited (the `comment` entries of GET /audit-log). Not a guarded action. Scopes: `review:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CreateCommentRequest` - `body` (string, required) — The comment's text. Trimmed; blank is refused. - `timestamp_seconds` (number or null) — Pin it to this point in the media, in seconds from the start; omitted or null for a comment on the asset as a whole. - `parent_id` (string (uuid) or null) — Reply to this comment (one on the same asset, from GET /api/v1/assets/{id}/comments); omitted or null for a top-level comment. Response 201 — `Comment`: A review comment on an asset. Comments belong to the asset, not a version: every version shows the same ones. - `id` (string (uuid), required) - `body` (string, required) - `author` (object or null, required) — Who wrote it: `id` is their principal in the org, `name` their display name. Null if the author's account is gone. A comment written with an org API key is authored by the org's API service principal, named `API`. - `id` (string (uuid), required) - `name` (string, required) - `timestamp_seconds` (number or null, required) — The point in the media the comment is pinned to, in seconds from the start; null for a comment on the asset as a whole. - `parent_id` (string (uuid) or null, required) — The comment this one replies to, or null for a top-level comment. The list is flat: thread it by `parent_id`. - `resolved_at` (string (date-time) or null, required) — When it was marked resolved, or null while open. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves when the body is edited or the comment is resolved or reopened. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 409 (Problem) — idempotency-in-flight. - 422 (Problem) — validation (`body` blank or too long, `timestamp_seconds`, `parent_id` not a comment on this asset), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### PATCH /api/v1/assets/{id}/comments/{comment_id} Resolve or reopen a comment, or change its text `resolved: true` marks it resolved, `false` reopens it; `body` replaces its text. For an OAuth token, its person may resolve, reopen (and delete) their own comments and any on an asset they may edit, and change the text of their own only; an API key may resolve or reopen any comment of its org and change the text only of comments written with the org's keys. Moves `updated_at`. Scopes: `review:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `comment_id` (path, string (uuid), required) — The comment's id, from GET /api/v1/assets/{id}/comments. Request body — `UpdateCommentRequest`: At least one of `resolved` and `body`. - `resolved` (boolean) — true marks it resolved (now), false reopens it. Setting the state it already has still moves updated_at. - `body` (string) — New text — the author's alone (for a key: comments the key's org wrote through the API). Response 200 — `Comment`: A review comment on an asset. Comments belong to the asset, not a version: every version shows the same ones. - `id` (string (uuid), required) - `body` (string, required) - `author` (object or null, required) — Who wrote it: `id` is their principal in the org, `name` their display name. Null if the author's account is gone. A comment written with an org API key is authored by the org's API service principal, named `API`. - `id` (string (uuid), required) - `name` (string, required) - `timestamp_seconds` (number or null, required) — The point in the media the comment is pinned to, in seconds from the start; null for a comment on the asset as a whole. - `parent_id` (string (uuid) or null, required) — The comment this one replies to, or null for a top-level comment. The list is flat: thread it by `parent_id`. - `resolved_at` (string (date-time) or null, required) — When it was marked resolved, or null while open. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves when the body is edited or the comment is resolved or reopened. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — not the author (for the text), or neither the author nor an editor of the asset. - 404 (Problem) — not-found. - 422 (Problem) — validation — neither `resolved` nor `body`, or a blank `body`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/assets/{id}/comments/{comment_id} Delete a comment Hard-deletes it, as the app does. A comment with replies cannot be deleted (409 `conflict`): delete the replies first, or resolve it. A second DELETE is a 404. For an OAuth token, its person's own comments and any on an asset they may edit; an API key, any comment of its org. Scopes: `review:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `comment_id` (path, string (uuid), required) — The comment's id, from GET /api/v1/assets/{id}/comments. Other responses: - 204 — Deleted. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — neither the author nor an editor of the asset. - 404 (Problem) — not-found. - 409 (Problem) — conflict — the comment has replies. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/assets/{id}/markers List an asset's clip markers The asset's clip markers (labelled time ranges; any of its versions), in timeline order unless `sort` says otherwise. Returned whole: the app allows at most 3 per asset. `updated_after` (compared to the millisecond) keeps only markers made after it — markers are not edited, only made and deleted (POST and DELETE under `review:write`). 404 for an asset the credential cannot read (for an OAuth token, read on the asset). Scopes: `review:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `updated_after` (query, string (date-time)) — Only markers changed strictly after this instant (ISO 8601 with offset), compared to the millisecond. - `sort` (query, "start_seconds" | "updated_at" | "-updated_at") — Default `start_seconds` (timeline order, as the app shows them); or `updated_at` / `-updated_at`. Ties break on id. Response 200 — `MarkerList` - `data` (array of Marker, required) — Returned whole: the app allows at most 3 markers per asset. - `id` (string (uuid), required) - `label` (string or null, required) - `start_seconds` (number, required) — Where the range starts, in seconds from the start of the media. - `end_seconds` (number, required) — Where it ends; always greater than start_seconds. - `created_by` (object or null, required) — Who marked it: `id` is their principal in the org, `name` their display name. Null if their account is gone. A marker made with an org API key names the org's API service principal, `API`. - `id` (string (uuid), required) - `name` (string, required) - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — A marker is never edited (made or deleted, in the app or the API), so this equals created_at. - `next_cursor` (string or null, required) — Always null: the list is returned whole. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `updated_after` or `sort`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/assets/{id}/markers Mark a time range on an asset Adds a clip marker as the credential (an OAuth token's person needs read on the asset; an API key marks as the org's API service principal, `API`). At most 3 per asset, as in the app: one more is 409 `conflict` — delete one first. Audited (the `marker` entries of GET /audit-log). Scopes: `review:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CreateMarkerRequest` - `start_seconds` (number, required) — Where the range starts, in seconds from the start of the media. - `end_seconds` (number, required) — Where it ends; must be greater than start_seconds. - `label` (string or null) — A short label; omitted or null for none. Response 201 — `Marker`: A clip marker: a labelled time range on an asset (the app's clips). Markers belong to the asset, not a version. - `id` (string (uuid), required) - `label` (string or null, required) - `start_seconds` (number, required) — Where the range starts, in seconds from the start of the media. - `end_seconds` (number, required) — Where it ends; always greater than start_seconds. - `created_by` (object or null, required) — Who marked it: `id` is their principal in the org, `name` their display name. Null if their account is gone. A marker made with an org API key names the org's API service principal, `API`. - `id` (string (uuid), required) - `name` (string, required) - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — A marker is never edited (made or deleted, in the app or the API), so this equals created_at. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 409 (Problem) — conflict — the asset already has 3 markers; or idempotency-in-flight. - 422 (Problem) — validation (`end_seconds` not after `start_seconds`, a negative time, a label over 200 characters), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/assets/{id}/markers/{marker_id} Delete a clip marker Hard-deletes it, as the app does. A second DELETE is a 404. For an OAuth token, its person's own markers and any on an asset they may edit; an API key, any marker of its org. Scopes: `review:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `marker_id` (path, string (uuid), required) — The marker's id, from GET /api/v1/assets/{id}/markers. Other responses: - 204 — Deleted. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — neither its creator nor an editor of the asset. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Asset share links #### GET /api/v1/assets/{id}/share-links List an asset's public share links Newest first: whether each has a password, its expiry, clip and download setting, how often it was opened, who made it. Never a link's URL or token — those are returned once, when the link is made — and never its password. For an OAuth token, its person must be able to share the asset (asset share), as in the app (403 otherwise; 404 when they cannot open it). Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page. Response 200 — `ShareLinkList`: A page of an asset's share links, newest first. - `data` (array of ShareLink, required) - `id` (string (uuid), required) - `asset` (object, required) — The asset it shares (its current version's id and name). - `id` (string (uuid), required) - `name` (string, required) - `has_password` (boolean, required) - `expires_at` (string (date-time) or null, required) — When it stops working; null: never. - `expired` (boolean, required) - `allow_download` (boolean, required) - `clip_start_seconds` (number or null, required) - `clip_end_seconds` (number or null, required) - `view_count` (integer, required) — How many times it was opened. - `created_by` (object or null, required) — Who made it: a member, or `API` for a link made with an org API key. Null if their account is gone. - `id` (string (uuid), required) - `name` (string, required) - `created_at` (string (date-time), required) - `next_cursor` (string or null, required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person cannot share the asset. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/assets/{id}/share-links Make a public share link to an asset (guarded) A guarded action (ADR 0081): anyone holding the link can watch the asset without signing in. The app's own write runs: a password is stored salted and hashed (the viewer enters it on the link's page; it is never sent in a URL); a clip pins the link to a cut; an asset under legal hold is 409 `legal-hold`. **An API key's link is made at once**: 201 with the link and its `url` — the ONLY time the URL is returned; keep it as you would a password. **An OAuth token's is not**: 202 with a Confirmation naming the asset, the expiry, download and password; once its person confirms it in impartire the link is made as them, and the confirmation's `result` is the CreatedShareLink with its `url` (readable only by the app and person that asked). Its person must be able to share the asset (no platform-admin bypass). Audited: a `share_link.created` event and the row's capture. Scopes: `sharing:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CreateShareLinkRequest` - `expires_in_days` (integer or null) — The link stops working this many days from now (1–365). Null or omitted: it never expires. - `password` (string or null) — A password the viewer must enter. Stored salted and hashed, never readable again. Null or omitted: none. - `allow_download` (boolean) — Whether the viewer may download the video. Default false. - `clip_start_seconds` (number or null) — Pin the link to a cut: its start, in seconds. Set with clip_end_seconds. - `clip_end_seconds` (number or null) — The cut's end, in seconds; after clip_start_seconds. Response 201 — `CreatedShareLink`: A share link just created, with its URL — the only time the URL is returned. - `id` (string (uuid), required) - `asset` (object, required) — The asset it shares (its current version's id and name). - `id` (string (uuid), required) - `name` (string, required) - `has_password` (boolean, required) - `expires_at` (string (date-time) or null, required) — When it stops working; null: never. - `expired` (boolean, required) - `allow_download` (boolean, required) - `clip_start_seconds` (number or null, required) - `clip_end_seconds` (number or null, required) - `view_count` (integer, required) — How many times it was opened. - `created_by` (object or null, required) — Who made it: a member, or `API` for a link made with an org API key. Null if their account is gone. - `id` (string (uuid), required) - `name` (string, required) - `created_at` (string (date-time), required) - `url` (string, required) — The public URL — the token in it is the link's whole authority. Returned ONCE, here (and in the confirmation's `result` for an OAuth token); never listed again. Keep it as you would a password. Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person cannot share the asset. - 404 (Problem) — not-found. - 409 (Problem) — legal-hold — the asset is held; confirmation-pending; or idempotency-in-flight. - 422 (Problem) — validation (a bad field; a clip with one end, or an end not after its start), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/assets/{id}/share-links/{link_id} Revoke a public share link (guarded) A guarded action (ADR 0081): the link stops working for everyone who has it, at once. For an OAuth token, the link's creator or a person who can share the asset, as in the app. **An API key's revoke happens at once** (204). **An OAuth token's does not**: 202 with a Confirmation; once its person confirms it in impartire the link is revoked as them. Audited: a `share_link.revoked` event and the row's capture. Scopes: `sharing:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `link_id` (path, string (uuid), required) — The link's id, from GET /api/v1/assets/{id}/share-links or its create. Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 204 — An API key's revoke: the link is gone. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — neither its creator nor someone who can share the asset. - 404 (Problem) — not-found. - 409 (Problem) — confirmation-pending. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Asset tags #### GET /api/v1/assets/{id}/tags List the tags on an asset Every version of an asset shows the same tags; the asset's ETag moves when its tags change. At most 500, ordered by the tag's normalized name; no cursor. Scopes: `assets:read` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). Response 200 — `TagLinkList` - `data` (array of TagLink, required) — Ordered by the tag's normalized name. At most 500. - `id` (string (uuid), required) — The tag's id — what DELETE …/tags/{tag_id} and `{ id }` on POST …/tags take. - `name` (string, required) — The tag as first typed. Tags are matched by a normalized form: case, extra spaces and invisible characters are ignored. - `scope` ("org" | "team", required) — `org`: the whole org's. `team`: private to one team (an OAuth token sees it only when its person is in the team or an org admin). - `team` (object or null, required) — The owning team, for a team tag; null for an org tag. - `id` (string (uuid), required) - `name` (string, required) - `usage_count` (integer, required) — How many assets and projects carry the tag, across the org. - `source` ("user" | "generated", required) — `user`: a person or an API credential added it. `generated`: applied from a generated summary. - `added_at` (string (date-time), required) - `next_cursor` (string or null, required) — Always null: the list is returned whole (at most 500). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/assets/{id}/tags Add tags to an asset Each item is `{ name }` — the org's tag of that name, created as an org tag when there is none — or `{ id }` of an existing tag (the only way to add a team tag). A tag already on the asset stays as it was, so the call is safe to repeat: no Idempotency-Key, no If-Match. Answers the asset's whole tag list after. An OAuth token's person needs edit on the asset. Scopes: `assets:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). Request body — `AddTagsRequest` - `tags` (array of object or object, required) — 1–50 tags, each `{ name }` or `{ id }`. - `name` (string, required) — A tag name. The org's tag of that name is used, or created as an org tag if there is none. Response 200 — `TagLinkList` - `data` (array of TagLink, required) — Ordered by the tag's normalized name. At most 500. - `id` (string (uuid), required) — The tag's id — what DELETE …/tags/{tag_id} and `{ id }` on POST …/tags take. - `name` (string, required) — The tag as first typed. Tags are matched by a normalized form: case, extra spaces and invisible characters are ignored. - `scope` ("org" | "team", required) — `org`: the whole org's. `team`: private to one team (an OAuth token sees it only when its person is in the team or an org admin). - `team` (object or null, required) — The owning team, for a team tag; null for an org tag. - `id` (string (uuid), required) - `name` (string, required) - `usage_count` (integer, required) — How many assets and projects carry the tag, across the org. - `source` ("user" | "generated", required) — `user`: a person or an API credential added it. `generated`: applied from a generated summary. - `added_at` (string (date-time), required) - `next_cursor` (string or null, required) — Always null: the list is returned whole (at most 500). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may see the asset but lacks edit on the asset. - 404 (Problem) — not-found. - 422 (Problem) — validation (`tags`: 1–50 items, each `{ name }` or `{ id }`; a name that is empty or over 64 characters), or unknown-tag. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/assets/{id}/tags/{tag_id} Remove a tag from an asset Takes the tag off the asset; the tag stays in the org's list. 404 when the asset does not carry it, so a second DELETE is a 404. Scopes: `assets:write` Parameters: - `id` (path, string (uuid), required) — The asset's id: its current version's id or its root id (an older version's id is not found). - `tag_id` (path, string (uuid), required) — The tag's id, from GET …/tags. Other responses: - 204 — Removed. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person lacks edit on the asset. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Workflows #### GET /api/v1/workflows List the workflow templates a run can be started from The org's live templates and the platform's system ones, ordered by name unless `sort` says otherwise (`updated_after` polls for changed ones). Each `param_schema` is what `params` on POST /runs is validated against. A template whose feature the org does not have is not listed; for an OAuth token, a team-private template only for a member of its team. Scopes: `workflows:read` Parameters: - `updated_after` (query, string (date-time)) — Only templates changed strictly after this instant (ISO 8601 with offset), compared to the millisecond — poll with the newest `updated_at` you have seen (that template does not come back). - `sort` (query, "name" | "-name" | "updated_at" | "-updated_at") — Sort key; a leading `-` is descending. Default `name`. Ties break on id. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `WorkflowList` - `data` (array of Workflow, required) - `id` (string (uuid), required) — The template's current version. Pass it as `workflow_id` to POST /api/v1/runs. - `name` (string, required) - `description` (string or null, required) - `target_asset_type` (object or null, required) — The asset type the template was built for (advisory: a run may be started on any asset). - `id` (string (uuid), required) - `name` (string, required) - `action_type` (string or null, required) — The media action the template runs, when it is a one-action template (e.g. transcode, captions); null for a multi-step process. - `param_schema` (map of object or null, required) — The launch parameters `params` on POST /api/v1/runs is validated against, keyed by name; null when the template takes none. - `updated_at` (string (date-time), required) — When the template (its current version) last changed — poll on it with GET /api/v1/workflows?updated_after=. - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `updated_after`, `sort`, `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/workflows/{id} Get one workflow template Scopes: `workflows:read` Parameters: - `id` (path, string (uuid), required) — The template's id, from GET /api/v1/workflows (any version's id resolves to the current version). Response 200 — `Workflow`: A workflow template a run can be started from. - `id` (string (uuid), required) — The template's current version. Pass it as `workflow_id` to POST /api/v1/runs. - `name` (string, required) - `description` (string or null, required) - `target_asset_type` (object or null, required) — The asset type the template was built for (advisory: a run may be started on any asset). - `id` (string (uuid), required) - `name` (string, required) - `action_type` (string or null, required) — The media action the template runs, when it is a one-action template (e.g. transcode, captions); null for a multi-step process. - `param_schema` (map of object or null, required) — The launch parameters `params` on POST /api/v1/runs is validated against, keyed by name; null when the template takes none. - `updated_at` (string (date-time), required) — When the template (its current version) last changed — poll on it with GET /api/v1/workflows?updated_after=. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Runs #### GET /api/v1/runs List workflow runs, most recently changed first Every run of the org — started through the API, in the app, or by an upload. For an OAuth token, the runs on assets its person can read and the runs they started; a run of a team-private template they are not in is listed with `private: true` and no detail. Poll for progress with `updated_after`. Scopes: `runs:read` Parameters: - `asset_id` (query, string (uuid)) — Only runs on this asset (any version of it). - `workflow_id` (query, string (uuid)) — Only runs of this template (any version of it). - `status` (query, "active" | "completed" | "cancelled" | "failed") — Only runs in this status. - `updated_after` (query, string (date-time)) — Only runs whose state changed strictly after this instant, compared to the millisecond — poll with the newest `updated_at` you have seen (that run does not come back). - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `RunList` - `data` (array of Run, required) - `id` (string (uuid), required) - `workflow` (object, required) — The template version the run is pinned to (any version's id names its template in GET /api/v1/workflows/{id}). `name` is null when the run is `private`. - `id` (string (uuid), required) - `name` (string or null, required) - `asset` (object, required) — The asset the run is about. `name` is null for an OAuth token whose person can no longer read the asset (a run they started). - `id` (string (uuid), required) - `name` (string or null, required) - `status` ("active" | "completed" | "cancelled" | "failed", required) - `private` (boolean, required) — True when the run's template is private to a team the token's person is not in: the run's existence, status, team and times are shown, its workflow name, steps, current step, incident and params are not (always false for an API key). - `team` (object or null, required) — The team that owns the run, snapshotted from its template at start; null for an unowned run. - `id` (string (uuid), required) - `name` (string, required) - `parent_run_id` (string (uuid) or null, required) — For a sub-workflow run, the run that started it. - `started_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves on every change of state — poll on it with GET /api/v1/runs?updated_after=. - `completed_at` (string (date-time) or null, required) - `started_via` (string, required) — api (started through this API) | manual (in the app) | trigger (by an arrival trigger, e.g. an upload). More may be added. - `current_step` (object or null, required) — The first open step at the run's current gate, while the run is active; null otherwise. - `id` (string (uuid), required) - `name` (string, required) - `status` (string, required) - `awaiting` ("approval" | "completion" | "automation" or null, required) — What the step waits on: `approval` (a person's verdict), `completion` (a person marking it done), `automation` (the pipeline — an automated step, a gate or a sub-workflow); null while it is parked on an incident. - `steps` (array of RunStep, required) - `id` (string (uuid), required) — The template step's id (of the template version the run is pinned to). - `name` (string, required) - `status` (string, required) — pending | in_progress | submitted | approved | completed | failed | skipped | incident (more may be added). - `incident` (object or null, required) — Set while an active run is parked on a step that failed and waits for an operator to retry it or fail the run (in the app). - `message` (string, required) - `params` (object (any keys) or null, required) — The run's launch parameters, as validated against the template's param_schema. - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad filter (`asset_id`, `workflow_id`, `status`, `updated_after`), `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/runs Start a workflow run on an asset Runs the app's start checks — the template and the asset exist in the org, `params` match the template's `param_schema` — then hands the run to the orchestrator. 202: the run has started, not finished; poll GET /runs/{id} until `status` leaves `active`, or until `current_step.awaiting` is `approval` or `completion` (a person's turn, in the app). A template that can deliver — its own action, a step's, or a sub_workflow child's, recursively — is refused for every credential (403 `confirmation-required`, naming the step): deliver against a requirement with POST /delivery-requirements/{id}/deliver; a social publish is done in the app. For an OAuth token its person needs create_workflow in the org and read on the asset. Scopes: `runs:write` Parameters: - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `StartRunRequest` - `workflow_id` (string (uuid), required) — A template from GET /api/v1/workflows. - `asset_id` (string (uuid), required) — The asset to run it on (its current version's id or its root id; the run is on the current version). - `params` (object (any keys)) — Launch parameters, validated against the template's `param_schema`. Response 202 — `Run`: A workflow run on an asset — one the API started, a person started in the app, or an upload set off. - `id` (string (uuid), required) - `workflow` (object, required) — The template version the run is pinned to (any version's id names its template in GET /api/v1/workflows/{id}). `name` is null when the run is `private`. - `id` (string (uuid), required) - `name` (string or null, required) - `asset` (object, required) — The asset the run is about. `name` is null for an OAuth token whose person can no longer read the asset (a run they started). - `id` (string (uuid), required) - `name` (string or null, required) - `status` ("active" | "completed" | "cancelled" | "failed", required) - `private` (boolean, required) — True when the run's template is private to a team the token's person is not in: the run's existence, status, team and times are shown, its workflow name, steps, current step, incident and params are not (always false for an API key). - `team` (object or null, required) — The team that owns the run, snapshotted from its template at start; null for an unowned run. - `id` (string (uuid), required) - `name` (string, required) - `parent_run_id` (string (uuid) or null, required) — For a sub-workflow run, the run that started it. - `started_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves on every change of state — poll on it with GET /api/v1/runs?updated_after=. - `completed_at` (string (date-time) or null, required) - `started_via` (string, required) — api (started through this API) | manual (in the app) | trigger (by an arrival trigger, e.g. an upload). More may be added. - `current_step` (object or null, required) — The first open step at the run's current gate, while the run is active; null otherwise. - `id` (string (uuid), required) - `name` (string, required) - `status` (string, required) - `awaiting` ("approval" | "completion" | "automation" or null, required) — What the step waits on: `approval` (a person's verdict), `completion` (a person marking it done), `automation` (the pipeline — an automated step, a gate or a sub-workflow); null while it is parked on an incident. - `steps` (array of RunStep, required) - `id` (string (uuid), required) — The template step's id (of the template version the run is pinned to). - `name` (string, required) - `status` (string, required) — pending | in_progress | submitted | approved | completed | failed | skipped | incident (more may be added). - `incident` (object or null, required) — Set while an active run is parked on a step that failed and waits for an operator to retry it or fail the run (in the app). - `message` (string, required) - `params` (object (any keys) or null, required) — The run's launch parameters, as validated against the template's param_schema. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, insufficient-permission, or confirmation-required — a template that reaches a delivery, with any credential (deliver with POST /delivery-requirements/{id}/deliver; publish a social delivery in the app). - 404 (Problem) — not-found — the template's action needs a feature the org does not have. - 409 (Problem) — idempotency-in-flight. - 422 (Problem) — unknown-workflow, unknown-asset, validation (`params` against the template's `param_schema`), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 502 (Problem) — orchestration-unavailable — nothing was started; retry. #### GET /api/v1/runs/{id} Get one workflow run: its status, current step and steps Scopes: `runs:read` Parameters: - `id` (path, string (uuid), required) — The run's id, from POST /api/v1/runs or GET /api/v1/runs. Response 200 — `Run`: A workflow run on an asset — one the API started, a person started in the app, or an upload set off. - `id` (string (uuid), required) - `workflow` (object, required) — The template version the run is pinned to (any version's id names its template in GET /api/v1/workflows/{id}). `name` is null when the run is `private`. - `id` (string (uuid), required) - `name` (string or null, required) - `asset` (object, required) — The asset the run is about. `name` is null for an OAuth token whose person can no longer read the asset (a run they started). - `id` (string (uuid), required) - `name` (string or null, required) - `status` ("active" | "completed" | "cancelled" | "failed", required) - `private` (boolean, required) — True when the run's template is private to a team the token's person is not in: the run's existence, status, team and times are shown, its workflow name, steps, current step, incident and params are not (always false for an API key). - `team` (object or null, required) — The team that owns the run, snapshotted from its template at start; null for an unowned run. - `id` (string (uuid), required) - `name` (string, required) - `parent_run_id` (string (uuid) or null, required) — For a sub-workflow run, the run that started it. - `started_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves on every change of state — poll on it with GET /api/v1/runs?updated_after=. - `completed_at` (string (date-time) or null, required) - `started_via` (string, required) — api (started through this API) | manual (in the app) | trigger (by an arrival trigger, e.g. an upload). More may be added. - `current_step` (object or null, required) — The first open step at the run's current gate, while the run is active; null otherwise. - `id` (string (uuid), required) - `name` (string, required) - `status` (string, required) - `awaiting` ("approval" | "completion" | "automation" or null, required) — What the step waits on: `approval` (a person's verdict), `completion` (a person marking it done), `automation` (the pipeline — an automated step, a gate or a sub-workflow); null while it is parked on an incident. - `steps` (array of RunStep, required) - `id` (string (uuid), required) — The template step's id (of the template version the run is pinned to). - `name` (string, required) - `status` (string, required) — pending | in_progress | submitted | approved | completed | failed | skipped | incident (more may be added). - `incident` (object or null, required) — Set while an active run is parked on a step that failed and waits for an operator to retry it or fail the run (in the app). - `message` (string, required) - `params` (object (any keys) or null, required) — The run's launch parameters, as validated against the template's param_schema. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/runs/{id}/signals Approve, reject, complete or skip a run's step, or cancel the run (guarded) A guarded action (ADR 0081). The app's own checks run: the step must be one of the run's and still open, the run active; for a person, the step's completion authority (who may sign for it), a skip's director-or-above rule, and standing on any `output_asset_id`. On an asset under legal hold, approve, reject, complete and skip are 409 `legal-hold` (cancel is always allowed). **An API key may only `cancel`**: it runs at once and answers 200 with the run (poll it until `status` is `cancelled`); any other kind is 403 `insufficient-permission` — attesting to work must name a person. **An OAuth token's signal does not run**: it answers 202 with a Confirmation and its Location; show the person `confirm_url`, then poll GET /confirmations/{id} — once they confirm, the signal runs as them and the confirmation's `result` is the run. The same ask while one waits is 409 `confirmation-pending`. Audited: a `run.signalled` event on the run. Scopes: `runs:signal` Parameters: - `id` (path, string (uuid), required) — The run's id, from POST /api/v1/runs or GET /api/v1/runs. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `SignalRunRequest` - `kind` ("approve" | "reject" | "complete" | "skip" | "cancel", required) — `approve` / `reject` an approval step, `complete` a manual step, `skip` a step (a director-or-above act; never an automated step), `cancel` the run. With an API key only `cancel` is allowed: the others attest to work and must name a person (an OAuth token, confirmed by them). - `workflow_step_id` (string (uuid)) — The step (from the run's `steps`): required for approve, reject, complete and skip. - `output_asset_id` (string (uuid)) — For `complete` or `approve` of a step whose requirements need a deliverable: the asset that satisfies them (one of the org's, which the person may edit). - `note` (string) — A note for the run's record (a skip's reason). Response 200 — `Run`: A workflow run on an asset — one the API started, a person started in the app, or an upload set off. - `id` (string (uuid), required) - `workflow` (object, required) — The template version the run is pinned to (any version's id names its template in GET /api/v1/workflows/{id}). `name` is null when the run is `private`. - `id` (string (uuid), required) - `name` (string or null, required) - `asset` (object, required) — The asset the run is about. `name` is null for an OAuth token whose person can no longer read the asset (a run they started). - `id` (string (uuid), required) - `name` (string or null, required) - `status` ("active" | "completed" | "cancelled" | "failed", required) - `private` (boolean, required) — True when the run's template is private to a team the token's person is not in: the run's existence, status, team and times are shown, its workflow name, steps, current step, incident and params are not (always false for an API key). - `team` (object or null, required) — The team that owns the run, snapshotted from its template at start; null for an unowned run. - `id` (string (uuid), required) - `name` (string, required) - `parent_run_id` (string (uuid) or null, required) — For a sub-workflow run, the run that started it. - `started_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves on every change of state — poll on it with GET /api/v1/runs?updated_after=. - `completed_at` (string (date-time) or null, required) - `started_via` (string, required) — api (started through this API) | manual (in the app) | trigger (by an arrival trigger, e.g. an upload). More may be added. - `current_step` (object or null, required) — The first open step at the run's current gate, while the run is active; null otherwise. - `id` (string (uuid), required) - `name` (string, required) - `status` (string, required) - `awaiting` ("approval" | "completion" | "automation" or null, required) — What the step waits on: `approval` (a person's verdict), `completion` (a person marking it done), `automation` (the pipeline — an automated step, a gate or a sub-workflow); null while it is parked on an incident. - `steps` (array of RunStep, required) - `id` (string (uuid), required) — The template step's id (of the template version the run is pinned to). - `name` (string, required) - `status` (string, required) — pending | in_progress | submitted | approved | completed | failed | skipped | incident (more may be added). - `incident` (object or null, required) — Set while an active run is parked on a step that failed and waits for an operator to retry it or fail the run (in the app). - `message` (string, required) - `params` (object (any keys) or null, required) — The run's launch parameters, as validated against the template's param_schema. Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an API key's attesting signal, or (on confirm) a person who may not sign for the step. - 404 (Problem) — not-found. - 409 (Problem) — conflict — the run is not active, the step is already closed, or the run has not reached the orchestrator; legal-hold — the asset is held (not for cancel); confirmation-pending; or idempotency-in-flight. - 422 (Problem) — validation (`workflow_step_id` missing or not a step of this run, an automated step to skip, a deliverable the step requires, `output_asset_id` with another kind), or idempotency-mismatch (an OAuth token may not send Idempotency-Key). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 502 (Problem) — orchestration-unavailable — the signal did not reach the orchestrator; nothing happened. ### Tags #### GET /api/v1/tags List the org's tags Org tags and team tags, ordered by name, each with how many assets and projects carry it. For an OAuth token, a team tag is listed only when its person is in the team (or an org admin). Scopes: `tags:read` Parameters: - `q` (query, string) — Only tags whose name starts with this, ignoring case and extra spaces. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `TagList` - `data` (array of Tag, required) - `id` (string (uuid), required) — The tag's id — what DELETE …/tags/{tag_id} and `{ id }` on POST …/tags take. - `name` (string, required) — The tag as first typed. Tags are matched by a normalized form: case, extra spaces and invisible characters are ignored. - `scope` ("org" | "team", required) — `org`: the whole org's. `team`: private to one team (an OAuth token sees it only when its person is in the team or an org admin). - `team` (object or null, required) — The owning team, for a team tag; null for an org tag. - `id` (string (uuid), required) - `name` (string, required) - `usage_count` (integer, required) — How many assets and projects carry the tag, across the org. - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Collections #### GET /api/v1/collections List the org's collections Ordered by name unless `sort` says otherwise. To poll for changes, pass `updated_after` with the newest `updated_at` you have seen and `sort=-updated_at` (or `updated_at`); `updated_at` moves when the name, description or constraints change, not when items do. A deleted collection leaves the list. For an OAuth token, the collections filed in a project its person can open. Scopes: `collections:read` Parameters: - `project_id` (query, string (uuid)) — Only collections filed in this project (GET /api/v1/projects). Not its sub-projects. - `parent_id` (query, string (uuid)) — Only the collections nested directly in this one. - `root_only` (query, "true" | "false") — `true`: only collections not nested in another. Default false. - `q` (query, string) — Case-insensitive substring of the collection's name. Characters are matched literally. - `updated_after` (query, string (date-time)) — Only collections changed strictly after this instant (ISO 8601 with offset), compared to the millisecond — poll with the newest `updated_at` you have seen (that collection does not come back), sorted `-updated_at` or `updated_at`. - `sort` (query, "name" | "-name" | "updated_at" | "-updated_at") — Sort key; a leading `-` is descending. Default `name`. Ties break on id. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `CollectionList` - `data` (array of Collection, required) - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `project` (ProjectRef or null, required) — The project the collection is filed in — its permission parent: who may open the project may open the collection. Null only for an OAuth token whose person owns the collection but cannot open its project. - `id` (string (uuid), required) - `name` (string, required) - `parent_id` (string (uuid) or null, required) — The collection this one is nested in (GET /collections/{id}/children lists a parent's children), or null for a top-level collection. - `status` ("draft" | "published", required) — `draft` until published to consumers. Publishing and archiving are done in the app. - `allowed_type_ids` (array of string (uuid) or null, required) — When set, only assets of these asset types (GET /api/v1/asset-types) may be added. Null or empty: any type. - `allowed_category_ids` (array of string (uuid) or null, required) — When set, only assets assigned at least one of these org categories may be added. Null or empty: any. - `item_count` (integer, required) — How many items the collection holds (GET /collections/{id}/items lists them). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or constraints last changed. Adding, removing or reordering items does not move it (the ETag moves instead). - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit`, `project_id`, `parent_id`, `root_only`, `updated_after`, `sort` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/collections Create a collection, filed in a project and optionally nested in another Born a draft. `project_id` is its permission parent (omitted: the org's Unfiled project); `parent_id` nests it for browsing only. The credential is the collection's owner: for an API key, the key's service principal. An OAuth token's person needs create_collection in the org, contribute on a named project, and update on a named parent collection. Scopes: `collections:write` Parameters: - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CreateCollectionRequest` - `name` (string, required) - `description` (string or null) - `project_id` (string (uuid)) — The project to file it in (GET /api/v1/projects) — its permission parent. Omit to file it in the org's Unfiled project, which every member can open. - `parent_id` (string (uuid)) — Nest it in this collection (GET /api/v1/collections). The nesting is for browsing only: who may open it is decided by its own project. - `allowed_type_ids` (array of string (uuid)) — Only assets of these types may be added (GET /api/v1/asset-types). - `allowed_category_ids` (array of string (uuid)) — Only assets assigned at least one of these org categories may be added. Response 201 — `Collection`: A curated, ordered set of assets. - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `project` (ProjectRef or null, required) — The project the collection is filed in — its permission parent: who may open the project may open the collection. Null only for an OAuth token whose person owns the collection but cannot open its project. - `id` (string (uuid), required) - `name` (string, required) - `parent_id` (string (uuid) or null, required) — The collection this one is nested in (GET /collections/{id}/children lists a parent's children), or null for a top-level collection. - `status` ("draft" | "published", required) — `draft` until published to consumers. Publishing and archiving are done in the app. - `allowed_type_ids` (array of string (uuid) or null, required) — When set, only assets of these asset types (GET /api/v1/asset-types) may be added. Null or empty: any type. - `allowed_category_ids` (array of string (uuid) or null, required) — When set, only assets assigned at least one of these org categories may be added. Null or empty: any. - `item_count` (integer, required) — How many items the collection holds (GET /collections/{id}/items lists them). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or constraints last changed. Adding, removing or reordering items does not move it (the ETag moves instead). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may not create collections, add to that project, or nest in that collection. - 404 (Problem) — not-found. - 409 (Problem) — idempotency-in-flight. - 422 (Problem) — validation (name, description, project_id, parent_id, allowed_type_ids, allowed_category_ids), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — authorization-write-failed — the collection's permissions could not be written, so it was rolled back; or internal. #### GET /api/v1/collections/{id} Get one collection Scopes: `collections:read` Parameters: - `id` (path, string (uuid), required) — The collection's id, from GET /api/v1/collections. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `Collection`: A curated, ordered set of assets. - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `project` (ProjectRef or null, required) — The project the collection is filed in — its permission parent: who may open the project may open the collection. Null only for an OAuth token whose person owns the collection but cannot open its project. - `id` (string (uuid), required) - `name` (string, required) - `parent_id` (string (uuid) or null, required) — The collection this one is nested in (GET /collections/{id}/children lists a parent's children), or null for a top-level collection. - `status` ("draft" | "published", required) — `draft` until published to consumers. Publishing and archiving are done in the app. - `allowed_type_ids` (array of string (uuid) or null, required) — When set, only assets of these asset types (GET /api/v1/asset-types) may be added. Null or empty: any type. - `allowed_category_ids` (array of string (uuid) or null, required) — When set, only assets assigned at least one of these org categories may be added. Null or empty: any. - `item_count` (integer, required) — How many items the collection holds (GET /collections/{id}/items lists them). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or constraints last changed. Adding, removing or reordering items does not move it (the ETag moves instead). Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### PATCH /api/v1/collections/{id} Rename or describe a collection, or change its constraints If-Match is required: send the ETag from the last GET (or from the create). Changing a constraint does not remove items already in the collection. `status` is not settable — publishing and archiving are lifecycle acts done in the app — and an unknown field is a 422. An OAuth token's person needs update on the collection. Scopes: `collections:write` Parameters: - `id` (path, string (uuid), required) — The collection's id, from GET /api/v1/collections. - `If-Match` (header, string, required) — The collection's ETag. Request body — `UpdateCollectionRequest`: At least one of name, description, allowed_type_ids, allowed_category_ids. `status` is not settable: publishing and archiving are lifecycle acts done in the app. Moving a collection is not in v1. - `name` (string) - `description` (string or null) — null clears it. - `allowed_type_ids` (array of string (uuid) or null) — null or [] lifts the constraint. Items already in the collection stay. - `allowed_category_ids` (array of string (uuid) or null) — null or [] lifts the constraint. Items already in the collection stay. Response 200 — `Collection`: A curated, ordered set of assets. - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `project` (ProjectRef or null, required) — The project the collection is filed in — its permission parent: who may open the project may open the collection. Null only for an OAuth token whose person owns the collection but cannot open its project. - `id` (string (uuid), required) - `name` (string, required) - `parent_id` (string (uuid) or null, required) — The collection this one is nested in (GET /collections/{id}/children lists a parent's children), or null for a top-level collection. - `status` ("draft" | "published", required) — `draft` until published to consumers. Publishing and archiving are done in the app. - `allowed_type_ids` (array of string (uuid) or null, required) — When set, only assets of these asset types (GET /api/v1/asset-types) may be added. Null or empty: any type. - `allowed_category_ids` (array of string (uuid) or null, required) — When set, only assets assigned at least one of these org categories may be added. Null or empty: any. - `item_count` (integer, required) — How many items the collection holds (GET /collections/{id}/items lists them). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or constraints last changed. Adding, removing or reordering items does not move it (the ETag moves instead). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the collection but not change it. - 404 (Problem) — not-found. - 412 (Problem) — precondition-failed — If-Match is stale; `etag` in the body is the current one. - 422 (Problem) — validation (empty body, an unknown field such as `status`, name, description, allowed_type_ids, allowed_category_ids). - 428 (Problem) — precondition-required — no If-Match. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/collections/{id} Delete a collection Soft-deletes the collection; it no longer appears in reads or lists. Its assets are untouched. No If-Match: a second DELETE is a 404. An OAuth token's person needs delete on the collection. Scopes: `collections:write` Parameters: - `id` (path, string (uuid), required) — The collection's id, from GET /api/v1/collections. Other responses: - 204 — Deleted. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the collection but not delete it. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/collections/{id}/children List the collections nested in a collection The collections nested directly in this one, by name. For an OAuth token, only those its person can open. Scopes: `collections:read` Parameters: - `id` (path, string (uuid), required) — The collection's id, from GET /api/v1/collections. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page. Response 200 — `CollectionList` - `data` (array of Collection, required) - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `project` (ProjectRef or null, required) — The project the collection is filed in — its permission parent: who may open the project may open the collection. Null only for an OAuth token whose person owns the collection but cannot open its project. - `id` (string (uuid), required) - `name` (string, required) - `parent_id` (string (uuid) or null, required) — The collection this one is nested in (GET /collections/{id}/children lists a parent's children), or null for a top-level collection. - `status` ("draft" | "published", required) — `draft` until published to consumers. Publishing and archiving are done in the app. - `allowed_type_ids` (array of string (uuid) or null, required) — When set, only assets of these asset types (GET /api/v1/asset-types) may be added. Null or empty: any type. - `allowed_category_ids` (array of string (uuid) or null, required) — When set, only assets assigned at least one of these org categories may be added. Null or empty: any. - `item_count` (integer, required) — How many items the collection holds (GET /collections/{id}/items lists them). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or constraints last changed. Adding, removing or reordering items does not move it (the ETag moves instead). - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/collections/{id}/items List a collection's items in order Ordered by `sort_order`, then by when each was added. Paginated: the list is bounded by the collection, a page at a time. Each item names its asset's current version. For an OAuth token, an item whose asset its person may not read keeps its place and id but is `restricted` with `asset: null`. Scopes: `collections:read` Parameters: - `id` (path, string (uuid), required) — The collection's id, from GET /api/v1/collections. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page. Response 200 — `CollectionItemList` - `data` (array of CollectionItem, required) - `id` (string (uuid), required) — The item's id — what DELETE …/items/{item_id} and PUT …/items/order take. - `asset` (object or null, required) — The asset (its current version). Null when `restricted`. - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `restricted` (boolean, required) — True for an OAuth token whose person may not read the asset: the item keeps its place and id (a curator can still move or remove it) but not the asset's identity. Always false for an API key. - `sort_order` (integer, required) — The item's position key; items are listed by it, ascending. - `added_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/collections/{id}/items Add an asset to a collection Pins the asset's current version. Without `sort_order` the item goes after the last one. The collection's constraints apply: a type not in `allowed_type_ids`, an asset with none of `allowed_category_ids`, or an unpublished asset in a published collection is 422 `collection-constraint`, `errors[].path` naming the rule. An asset already in the collection (any version of it) is 409 `already-member`. An OAuth token's person needs update on the collection and read on the asset. Scopes: `collections:write` Parameters: - `id` (path, string (uuid), required) — The collection's id, from GET /api/v1/collections. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `AddCollectionItemRequest` - `asset_id` (string (uuid), required) — The asset to add (its current version's id or its root id; the item pins the current version). GET /api/v1/assets lists them. - `sort_order` (integer) — Its position key. Omit to append after the last item. Response 201 — `CollectionItem`: One item of a collection: an asset at a position. - `id` (string (uuid), required) — The item's id — what DELETE …/items/{item_id} and PUT …/items/order take. - `asset` (object or null, required) — The asset (its current version). Null when `restricted`. - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `restricted` (boolean, required) — True for an OAuth token whose person may not read the asset: the item keeps its place and id (a curator can still move or remove it) but not the asset's identity. Always false for an API key. - `sort_order` (integer, required) — The item's position key; items are listed by it, ascending. - `added_at` (string (date-time), required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the collection but not change it. - 404 (Problem) — not-found. - 409 (Problem) — already-member — the asset is already in the collection (`item_id` in the body); or idempotency-in-flight. - 422 (Problem) — collection-constraint, unknown-asset, validation (asset_id, sort_order), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/collections/{id}/items/{item_id} Remove an item from a collection The asset itself is untouched. A second DELETE is a 404. An OAuth token's person needs update on the collection. Scopes: `collections:write` Parameters: - `id` (path, string (uuid), required) — The collection's id, from GET /api/v1/collections. - `item_id` (path, string (uuid), required) — The item's id, from GET /api/v1/collections/{id}/items. Other responses: - 204 — Removed. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the collection but not change it. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### PUT /api/v1/collections/{id}/items/order Put a collection's items in a new order `item_ids` is every item of the collection, each exactly once, in the new order; their `sort_order` becomes 0, 1, 2… Anything else (a missing, repeated or foreign id) is a 422. A collection of more than 1000 items is reordered in the app. Answers the whole list after. If an item was added or removed meanwhile, nothing moves and the answer is 409 `conflict`: read the items and send the order again. Safe to repeat: no Idempotency-Key, no If-Match. An OAuth token's person needs update on the collection. Scopes: `collections:write` Parameters: - `id` (path, string (uuid), required) — The collection's id, from GET /api/v1/collections. Request body — `ReorderCollectionItemsRequest` - `item_ids` (array of string (uuid), required) — Every item id of the collection, each exactly once, in the new order (at most 1000). The items' sort_order becomes 0, 1, 2… in this order. Response 200 — `CollectionItemList` - `data` (array of CollectionItem, required) - `id` (string (uuid), required) — The item's id — what DELETE …/items/{item_id} and PUT …/items/order take. - `asset` (object or null, required) — The asset (its current version). Null when `restricted`. - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `restricted` (boolean, required) — True for an OAuth token whose person may not read the asset: the item keeps its place and id (a curator can still move or remove it) but not the asset's identity. Always false for an API key. - `sort_order` (integer, required) — The item's position key; items are listed by it, ascending. - `added_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the collection but not change it. - 404 (Problem) — not-found. - 409 (Problem) — conflict — the collection's items changed while it was being reordered; nothing moved. - 422 (Problem) — validation — `item_ids` is not every item exactly once, or the collection is too large to reorder here. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Playlists #### GET /api/v1/playlists List a project's playlists `project_id` is required: playlists are listed one project at a time. Ordered by name unless `sort` says otherwise. To poll for changes, pass `updated_after` with the newest `updated_at` you have seen and `sort=-updated_at` (or `updated_at`); `updated_at` moves when the name, description or target_count change, not when items do. A deleted playlist leaves the list. An OAuth token's person needs read on the project (a project they cannot open is a 422 like a missing one). Scopes: `projects:read` Parameters: - `project_id` (query, string (uuid), required) — Required: the project whose playlists to list (GET /api/v1/projects). Not its sub-projects. - `updated_after` (query, string (date-time)) — Only playlists changed strictly after this instant (ISO 8601 with offset), compared to the millisecond — pass the newest `updated_at` you have seen and that playlist is not returned again. - `sort` (query, "name" | "-name" | "updated_at" | "-updated_at") — Sort key; a leading `-` is descending. Default `name`. Ties break on id. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `PlaylistList` - `data` (array of Playlist, required) - `id` (string (uuid), required) - `project` (ProjectRef, required) — The project the playlist belongs to — its permission parent: who may open the project may open the playlist, who may update the project may change it. - `id` (string (uuid), required) - `name` (string, required) - `name` (string, required) - `description` (string or null, required) - `target_count` (integer or null, required) — How many items the playlist is meant to hold (e.g. a season's episode count), or null. Advisory: nothing refuses an item past it. - `item_count` (integer, required) — How many items it holds (GET /playlists/{id}/items lists them). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or target_count last changed. Adding, removing or reordering items does not move it (the ETag moves instead). - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a missing or unknown `project_id`, or a bad `limit`, `updated_after`, `sort` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/playlists Create a playlist in a project A typed project's type must allow playlists (an untyped project always does); otherwise 422 `project-type-disallows`. An OAuth token's person needs update on the project. Scopes: `projects:write` Parameters: - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CreatePlaylistRequest` - `project_id` (string (uuid), required) — The project it belongs to (GET /api/v1/projects). A typed project's type must allow playlists. - `name` (string, required) - `description` (string or null) - `target_count` (integer or null) — How many items it is meant to hold; advisory. Response 201 — `Playlist`: An ordered, count-aware sequence of assets belonging to a project (e.g. a season's episodes). - `id` (string (uuid), required) - `project` (ProjectRef, required) — The project the playlist belongs to — its permission parent: who may open the project may open the playlist, who may update the project may change it. - `id` (string (uuid), required) - `name` (string, required) - `name` (string, required) - `description` (string or null, required) - `target_count` (integer or null, required) — How many items the playlist is meant to hold (e.g. a season's episode count), or null. Advisory: nothing refuses an item past it. - `item_count` (integer, required) — How many items it holds (GET /playlists/{id}/items lists them). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or target_count last changed. Adding, removing or reordering items does not move it (the ETag moves instead). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it. - 404 (Problem) — not-found. - 409 (Problem) — idempotency-in-flight. - 422 (Problem) — project-type-disallows, validation (project_id, name, description, target_count), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/playlists/{id} Get one playlist Scopes: `projects:read` Parameters: - `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `Playlist`: An ordered, count-aware sequence of assets belonging to a project (e.g. a season's episodes). - `id` (string (uuid), required) - `project` (ProjectRef, required) — The project the playlist belongs to — its permission parent: who may open the project may open the playlist, who may update the project may change it. - `id` (string (uuid), required) - `name` (string, required) - `name` (string, required) - `description` (string or null, required) - `target_count` (integer or null, required) — How many items the playlist is meant to hold (e.g. a season's episode count), or null. Advisory: nothing refuses an item past it. - `item_count` (integer, required) — How many items it holds (GET /playlists/{id}/items lists them). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or target_count last changed. Adding, removing or reordering items does not move it (the ETag moves instead). Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### PATCH /api/v1/playlists/{id} Rename or describe a playlist, or change its target count If-Match is required: send the ETag from the last GET (or from the create). An unknown field is a 422; moving a playlist to another project is not in v1. An OAuth token's person needs update on the playlist's project. Scopes: `projects:write` Parameters: - `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=. - `If-Match` (header, string, required) — The playlist's ETag. Request body — `UpdatePlaylistRequest`: At least one of name, description, target_count. Moving a playlist to another project is not in v1; an unknown field is a 422. - `name` (string) - `description` (string or null) — null clears it. - `target_count` (integer or null) — null clears it. Response 200 — `Playlist`: An ordered, count-aware sequence of assets belonging to a project (e.g. a season's episodes). - `id` (string (uuid), required) - `project` (ProjectRef, required) — The project the playlist belongs to — its permission parent: who may open the project may open the playlist, who may update the project may change it. - `id` (string (uuid), required) - `name` (string, required) - `name` (string, required) - `description` (string or null, required) - `target_count` (integer or null, required) — How many items the playlist is meant to hold (e.g. a season's episode count), or null. Advisory: nothing refuses an item past it. - `item_count` (integer, required) — How many items it holds (GET /playlists/{id}/items lists them). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — When its name, description or target_count last changed. Adding, removing or reordering items does not move it (the ETag moves instead). Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it. - 404 (Problem) — not-found. - 412 (Problem) — precondition-failed — If-Match is stale; `etag` in the body is the current one. - 422 (Problem) — validation (empty body, an unknown field, name, description, target_count). - 428 (Problem) — precondition-required — no If-Match. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/playlists/{id} Delete a playlist Soft-deletes the playlist; it no longer appears in reads or lists. Its assets are untouched. No If-Match: a second DELETE is a 404. An OAuth token's person needs update on the playlist's project. Scopes: `projects:write` Parameters: - `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=. Other responses: - 204 — Deleted. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/playlists/{id}/items List a playlist's items in order Ordered by `sort_order`, then by when each was added. Paginated: the list is bounded by the playlist, a page at a time. Each item names its asset's current version. For an OAuth token, an item whose asset its person may not read keeps its place and id but is `restricted` with `asset: null`. Scopes: `projects:read` Parameters: - `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page. Response 200 — `PlaylistItemList` - `data` (array of PlaylistItem, required) - `id` (string (uuid), required) — The item's id — what DELETE …/items/{item_id} and PUT …/items/order take. - `asset` (object or null, required) — The asset (its current version). Null when `restricted`. - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `restricted` (boolean, required) — True for an OAuth token whose person may not read the asset (a playlist in an open project may hold material from a private one): the item keeps its place and id but not the asset's identity. Always false for an API key. - `sort_order` (integer, required) — The item's place in the list; items are listed by it, ascending. - `position` (integer or null, required) — An optional editorial slot number (e.g. episode 3), independent of the list order. - `added_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/playlists/{id}/items Append an asset to a playlist Pins the asset's current version and appends it after the last item; `position` is an optional editorial slot number, independent of the order. An asset already in the playlist (any version of it) is 409 `already-member`. An OAuth token's person needs update on the playlist's project and read on the asset. Scopes: `projects:write` Parameters: - `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `AddPlaylistItemRequest` - `asset_id` (string (uuid), required) — The asset to add (its current version's id or its root id; the item pins the current version). GET /api/v1/assets lists them. - `position` (integer or null) — An editorial slot number (e.g. episode 3). The item is appended to the list whatever its position. Response 201 — `PlaylistItem`: One item of a playlist: an asset at a place in the list. - `id` (string (uuid), required) — The item's id — what DELETE …/items/{item_id} and PUT …/items/order take. - `asset` (object or null, required) — The asset (its current version). Null when `restricted`. - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `restricted` (boolean, required) — True for an OAuth token whose person may not read the asset (a playlist in an open project may hold material from a private one): the item keeps its place and id but not the asset's identity. Always false for an API key. - `sort_order` (integer, required) — The item's place in the list; items are listed by it, ascending. - `position` (integer or null, required) — An optional editorial slot number (e.g. episode 3), independent of the list order. - `added_at` (string (date-time), required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it. - 404 (Problem) — not-found. - 409 (Problem) — already-member — the asset is already in the playlist (`item_id` in the body); or idempotency-in-flight. - 422 (Problem) — unknown-asset, validation (asset_id, position), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/playlists/{id}/items/{item_id} Remove an item from a playlist The asset itself is untouched; the other items keep their `sort_order`. A second DELETE is a 404. An OAuth token's person needs update on the playlist's project. Scopes: `projects:write` Parameters: - `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=. - `item_id` (path, string (uuid), required) — The item's id, from GET /api/v1/playlists/{id}/items. Other responses: - 204 — Removed. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### PUT /api/v1/playlists/{id}/items/order Put a playlist's items in a new order `item_ids` is every item of the playlist, each exactly once, in the new order; their `sort_order` becomes 0, 1, 2… (`position` is untouched). Anything else (a missing, repeated or foreign id) is a 422. A playlist of more than 1000 items is reordered in the app. Answers the whole list after. If an item was added or removed meanwhile, nothing moves and the answer is 409 `conflict`: read the items and send the order again. Safe to repeat: no Idempotency-Key, no If-Match. An OAuth token's person needs update on the playlist's project. Scopes: `projects:write` Parameters: - `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=. Request body — `ReorderPlaylistItemsRequest` - `item_ids` (array of string (uuid), required) — Every item id of the playlist, each exactly once, in the new order (at most 1000). The items' sort_order becomes 0, 1, 2… in this order; position is untouched. Response 200 — `PlaylistItemList` - `data` (array of PlaylistItem, required) - `id` (string (uuid), required) — The item's id — what DELETE …/items/{item_id} and PUT …/items/order take. - `asset` (object or null, required) — The asset (its current version). Null when `restricted`. - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `restricted` (boolean, required) — True for an OAuth token whose person may not read the asset (a playlist in an open project may hold material from a private one): the item keeps its place and id but not the asset's identity. Always false for an API key. - `sort_order` (integer, required) — The item's place in the list; items are listed by it, ascending. - `position` (integer or null, required) — An optional editorial slot number (e.g. episode 3), independent of the list order. - `added_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it. - 404 (Problem) — not-found. - 409 (Problem) — conflict — the playlist's items changed while it was being reordered; nothing moved. - 422 (Problem) — validation — `item_ids` is not every item exactly once, or the playlist is too large to reorder here. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Record types #### GET /api/v1/record-types List the org's record types and their fields Every live record type of the org (custom objects: a Product, a Contract…), by name, each with its live field definitions in the app's form order — the contract `fields` on POST / PATCH /records is validated against. A `reference` field names its target in `reference_target`. Deprecated definitions, and for an OAuth token the ones its person may not read, are left out. The list is returned whole. All 404 when the org does not have custom records. Scopes: `records:read` Response 200 — `RecordTypeList` - `data` (array of RecordType, required) - `id` (string (uuid), required) - `name` (string, required) - `kind` ("record", required) - `fields` (array of RecordField, required) — The type's live field definitions, in the app's form order. Deprecated fields, and for an OAuth token the fields its person may not read, are left out. - `id` (string (uuid), required) - `key` (string, required) — The key a value is stored under in an asset's `fields`. - `name` (string, required) — The field's label, as the app shows it. - `type` (string, required) — text | url (a string) · select (one option `value`) · multiselect (an array of option values) · date (YYYY-MM-DD) · datetime (epoch milliseconds) · number · reference (an id, or an array of ids when `multiple`). More may be added. - `required` (boolean, required) — Required for the asset to be ready (a workflow gate may wait on it). Not enforced on a write: an asset may be created empty and filled in later. - `multiple` (boolean, required) — For a reference field, whether the value is an array of ids. - `options` (array of object or null, required) — For select and multiselect, the accepted values (store `value`; `label` is for display). Null for every other type. - `value` (string, required) - `label` (string, required) - `reference_target` (object or null, required) — For a `reference` field, its target. Null for every other type. - `kind` ("asset" | "record" | "project", required) — What the field points at: an asset, a record, or a project of the org. - `type_id` (string (uuid) or null, required) — For `record`, the record type the target must be of (GET /api/v1/record-types); null otherwise. - `next_cursor` (string or null, required) — Always null: the list is returned whole. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Records #### GET /api/v1/records List one record type's records `type_id` is required: records are listed one type at a time. Newest first unless `sort` says otherwise. To poll for changes, pass `updated_after` with the newest `updated_at` you have seen and `sort=-updated_at` (or `updated_at`); a deleted record leaves the list. Each record carries its `fields` and its `references` resolved to names. For an OAuth token, records its person may not read are left out (a page may be short and still have a next one) and the fields they may not read are stripped. All 404 when the org does not have custom records. Scopes: `records:read` Parameters: - `type_id` (query, string (uuid), required) — Required: the record type whose records to list (GET /api/v1/record-types). - `q` (query, string) — Case-insensitive substring of the record's title. Characters are matched literally. - `project_id` (query, string (uuid)) — Only records filed under this project (GET /api/v1/projects). - `updated_after` (query, string (date-time)) — Only records changed strictly after this instant (ISO 8601 with offset), compared to the millisecond — pass the newest `updated_at` you have seen and that record is not returned again. - `sort` (query, "created_at" | "-created_at" | "title" | "-title" | "updated_at" | "-updated_at") — Sort key; a leading `-` is descending. Default `-created_at` (newest first, as the app lists them). Ties break on id. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `RecordList` - `data` (array of Record, required) - `id` (string (uuid), required) - `type` (object, required) — Its record type (GET /api/v1/record-types). - `id` (string (uuid), required) - `name` (string, required) - `title` (string, required) - `fields` (object (any keys), required) — Field values keyed by field key, as defined by the record type. For an OAuth token, the fields its person may not read are left out. - `references` (array of RecordReference, required) — Every value of the reference fields in `fields`, resolved to a name, in the type's field order then value order. - `field_key` (string, required) — The reference field the value is stored under in `fields`. - `record` (object or null, required) — Set when the field points at a record. - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `asset` (object or null, required) — Set when the field points at an asset (by the id stored in `fields`). - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `project` (object or null, required) — Set when the field points at a project. - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `project` (ProjectRef or null, required) — A project the record is filed under, or null. Informational: records are org-wide, and who may read one does not depend on it. - `id` (string (uuid), required) - `name` (string, required) - `edit_seq` (integer, required) — Increments on every change to title, fields, type or project. The first part of the ETag; If-Match on PATCH compares it. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. For an OAuth token a page may hold fewer than `limit` records (those its person may not read are left out) and still have a next page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a missing or unknown `type_id` (an asset type's id included), an unknown `project_id`, or a bad `limit`, `updated_after`, `sort` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/records Create a record `fields` is validated against the record type (GET /record-types): a reference field holds the target's id (an array when `multiple`) — a live asset, a record of the declared type, or a project of the org. Its reference edges are written with the record. `project_id` files it under a project; it does not change who may read it (records are org-wide). An OAuth token's person needs create_asset in the org (the app's permission for records), and may not send a field they cannot read. All 404 when the org does not have custom records. Scopes: `records:write` Parameters: - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CreateRecordRequest` - `type_id` (string (uuid), required) — A record type from GET /api/v1/record-types. - `title` (string, required) — Leading and trailing spaces are trimmed; it may not be blank. - `fields` (object (any keys)) — Field values keyed by field key, validated against the record type (a reference: the target's id, or an array of ids when `multiple`). - `project_id` (string (uuid)) — File the record under this project (GET /api/v1/projects). Informational: it does not change who may read it. Response 201 — `Record`: An instance of a record type: a title, a field bag and resolved references. - `id` (string (uuid), required) - `type` (object, required) — Its record type (GET /api/v1/record-types). - `id` (string (uuid), required) - `name` (string, required) - `title` (string, required) - `fields` (object (any keys), required) — Field values keyed by field key, as defined by the record type. For an OAuth token, the fields its person may not read are left out. - `references` (array of RecordReference, required) — Every value of the reference fields in `fields`, resolved to a name, in the type's field order then value order. - `field_key` (string, required) — The reference field the value is stored under in `fields`. - `record` (object or null, required) — Set when the field points at a record. - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `asset` (object or null, required) — Set when the field points at an asset (by the id stored in `fields`). - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `project` (object or null, required) — Set when the field points at a project. - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `project` (ProjectRef or null, required) — A project the record is filed under, or null. Informational: records are org-wide, and who may read one does not depend on it. - `id` (string (uuid), required) - `name` (string, required) - `edit_seq` (integer, required) — Increments on every change to title, fields, type or project. The first part of the ETag; If-Match on PATCH compares it. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may not create records, or sent a field they cannot read. - 404 (Problem) — not-found. - 409 (Problem) — idempotency-in-flight. - 422 (Problem) — validation (type_id, title, project_id, or `fields.` naming each invalid value), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — authorization-write-failed — the record's permissions could not be written, so it was rolled back; or internal. #### GET /api/v1/records/{id} Get one record Scopes: `records:read` Parameters: - `id` (path, string (uuid), required) — The record's id, from GET /api/v1/records?type_id=. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `Record`: An instance of a record type: a title, a field bag and resolved references. - `id` (string (uuid), required) - `type` (object, required) — Its record type (GET /api/v1/record-types). - `id` (string (uuid), required) - `name` (string, required) - `title` (string, required) - `fields` (object (any keys), required) — Field values keyed by field key, as defined by the record type. For an OAuth token, the fields its person may not read are left out. - `references` (array of RecordReference, required) — Every value of the reference fields in `fields`, resolved to a name, in the type's field order then value order. - `field_key` (string, required) — The reference field the value is stored under in `fields`. - `record` (object or null, required) — Set when the field points at a record. - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `asset` (object or null, required) — Set when the field points at an asset (by the id stored in `fields`). - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `project` (object or null, required) — Set when the field points at a project. - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `project` (ProjectRef or null, required) — A project the record is filed under, or null. Informational: records are org-wide, and who may read one does not depend on it. - `id` (string (uuid), required) - `name` (string, required) - `edit_seq` (integer, required) — Increments on every change to title, fields, type or project. The first part of the ETag; If-Match on PATCH compares it. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### PATCH /api/v1/records/{id} Retitle a record or replace its fields If-Match is required: the ETag from the last GET (or the create), or the bare `edit_seq`. `fields` replaces the whole bag (send back what GET returned, changed) and the reference edges follow it. An unknown field (`type_id`, `project_id`) is a 422. An OAuth token's person needs edit on the record; a field they cannot read is kept as it was and may not be sent. All 404 when the org does not have custom records. Scopes: `records:write` Parameters: - `id` (path, string (uuid), required) — The record's id, from GET /api/v1/records?type_id=. - `If-Match` (header, string, required) — The record's ETag, or its bare edit_seq. Request body — `UpdateRecordRequest`: At least one of title, fields. Changing a record's type or project is not in v1; an unknown field is a 422. - `title` (string) — Leading and trailing spaces are trimmed; it may not be blank. - `fields` (object (any keys)) — The record's whole field bag, validated against its type: keys left out are removed (send back what GET returned, changed). Legacy `__legacy` values are carried forward unless sent as null. For an OAuth token, a field its person may not read is kept as it was. Response 200 — `Record`: An instance of a record type: a title, a field bag and resolved references. - `id` (string (uuid), required) - `type` (object, required) — Its record type (GET /api/v1/record-types). - `id` (string (uuid), required) - `name` (string, required) - `title` (string, required) - `fields` (object (any keys), required) — Field values keyed by field key, as defined by the record type. For an OAuth token, the fields its person may not read are left out. - `references` (array of RecordReference, required) — Every value of the reference fields in `fields`, resolved to a name, in the type's field order then value order. - `field_key` (string, required) — The reference field the value is stored under in `fields`. - `record` (object or null, required) — Set when the field points at a record. - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `asset` (object or null, required) — Set when the field points at an asset (by the id stored in `fields`). - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `project` (object or null, required) — Set when the field points at a project. - `id` (string (uuid), required) - `name` (string or null, required) — The target's name (a record's title; a sub-project's as ` › ` when the parent is readable too). Null when the target is gone or — for an OAuth token — its person may not read it: the id is already in `fields`, the name is not disclosed. - `project` (ProjectRef or null, required) — A project the record is filed under, or null. Informational: records are org-wide, and who may read one does not depend on it. - `id` (string (uuid), required) - `name` (string, required) - `edit_seq` (integer, required) — Increments on every change to title, fields, type or project. The first part of the ETag; If-Match on PATCH compares it. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may read the record but not edit it, or sent a field they cannot read. - 404 (Problem) — not-found. - 412 (Problem) — precondition-failed — If-Match is stale; `edit_seq` in the body is the current one. - 422 (Problem) — validation (empty body, an unknown field, title, or `fields.` naming each invalid value). - 428 (Problem) — precondition-required — no If-Match. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/records/{id} Delete a record Soft-deletes the record; it no longer appears in reads, lists or referenced-by. No If-Match: a second DELETE is a 404. An OAuth token's person needs delete on the record. All 404 when the org does not have custom records. Scopes: `records:write` Parameters: - `id` (path, string (uuid), required) — The record's id, from GET /api/v1/records?type_id=. Other responses: - 204 — Deleted. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may read the record but not delete it. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### People and credits #### GET /api/v1/people List the org's people The org's people (talent, crew, contributors) — archived ones left out — by name unless `sort` says otherwise. `q` searches name and bio; `nationality` matches exactly. To poll, pass `updated_after` with the newest `updated_at` you have seen (compared to the millisecond, so that person does not come back) and `sort=updated_at`; an archived person leaves the list. For an OAuth token, empty unless its person may read the org's people. Read only. 404 when the org does not have people. Scopes: `people:read` Parameters: - `q` (query, string) — Case-insensitive substring of the name or the bio (as the app's people search). - `nationality` (query, string) — Exact nationality, as the app stores it. - `updated_after` (query, string (date-time)) — Only people changed strictly after this instant (ISO 8601 with offset), compared to the millisecond — pass the newest `updated_at` you have seen and that person is not returned again. - `sort` (query, "name" | "-name" | "updated_at" | "-updated_at") — Default `name` (case-insensitive, the app's order); `-name`, `updated_at`, `-updated_at`. Ties break on id. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `PersonList` - `data` (array of Person, required) - `id` (string (uuid), required) - `name` (string, required) - `bio` (string or null, required) - `nationality` (string or null, required) — As entered in the app (free text, e.g. `British`). - `birth_date` (string (date) or null, required) — YYYY-MM-DD, or null. - `credit_count` (integer, required) — How many credits the person holds on live assets of the org — org-wide, whoever asks (as the app's people list counts). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves when the person is edited in the app. - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `updated_after`, `sort`, `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/people/{id} Get a person One person of the org. Archived, foreign or (for an OAuth token whose person may not read the org's people) unreadable → 404. Send the ETag back in If-None-Match for a 304. 404 when the org does not have people. Scopes: `people:read` Parameters: - `id` (path, string (uuid), required) — The person's id, from GET /api/v1/people. Response 200 — `Person`: A person (talent, crew, contributor) the org keeps a record of. Archived people are not served. - `id` (string (uuid), required) - `name` (string, required) - `bio` (string or null, required) - `nationality` (string or null, required) — As entered in the app (free text, e.g. `British`). - `birth_date` (string (date) or null, required) — YYYY-MM-DD, or null. - `credit_count` (integer, required) — How many credits the person holds on live assets of the org — org-wide, whoever asks (as the app's people list counts). - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves when the person is edited in the app. Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/people/{id}/credits List a person's credits The credits the person holds, from the asset's side — each asset (its current version) with its type, role and billing order — by asset name, paginated. A credit on a deleted asset is not listed. For an OAuth token, only assets its person may read. 404 for a person the credential cannot read. 404 when the org does not have people. Scopes: `people:read` Parameters: - `id` (path, string (uuid), required) — The person's id, from GET /api/v1/people. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page. Response 200 — `PersonCreditList` - `data` (array of PersonCredit, required) - `id` (string (uuid), required) — The credit's id. - `asset` (object, required) — The credited asset (its current version) and its type. - `id` (string (uuid), required) - `name` (string, required) - `type` (object, required) - `id` (string (uuid), required) - `name` (string, required) - `role` (string or null, required) - `billing_order` (integer or null, required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Categories #### GET /api/v1/categories List the org's asset categories The org's category taxonomy for assets — categories whose group is for assets or for every kind of thing — each with its group, by group (as the app orders them) then name. Returned whole, at most 1000. Use an id with `GET /assets?category_id=`. Read only: the taxonomy is managed in the app. For an OAuth token, empty unless its person is a member of the org. 404 when the org does not have categories. Scopes: `taxonomy:read` Response 200 — `CategoryList` - `data` (array of Category, required) — By group (its order in the app, then name), then name. Returned whole, at most 1000. - `group` (object, required) — The category group (a facet: Genre, Territory…). - `id` (string (uuid), required) - `name` (string, required) - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `parent_id` (string (uuid) or null, required) — The category this one sits under (one level deep at most), or null. - `is_system` (boolean, required) — A category the platform seeded; it cannot be deleted in the app. - `next_cursor` (string or null, required) — Always null: the list is returned whole. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Audit log #### GET /api/v1/audit-log List the org's audit trail Who changed what and when, newest first, as a projected feed: each entry names the resource (a public type, its id and name), the actor, the request it belongs to, a semantic event's public ids where there is one, and the public fields that changed — never the stored row images, and only the KEYS of an asset's or a record's `fields`. Something the feed does not describe is `resource.type: other` with no name and no changes. Filters combine with AND; `since` / `until` are exclusive. An `event` entry's changes are on the entry with the same `request_id`. For an OAuth token, its person needs the view-audit-log permission (no platform-admin bypass). 404 when the org does not have the audit log, and for an OAuth token whose person may not view it. Scopes: `audit:read` Parameters: - `resource_type` (query, "asset" | "file" | "project" | "collection" | "playlist" | "record" | "tag" | "tag_link" | "asset_type" | "field_definition" | "workflow" | "spec_profile" | "api_key" | "run" | "receipt" | "delivery_requirement" | "delivery_target" | "schedule_slot" | "service" | "channel" | "share_link" | "comment" | "marker" | "confirmation" | "other") — Only entries about this kind of resource. - `resource_id` (query, string) — Only entries about this resource (its id as `resource.id` shows it — for an asset, a version's id). - `actor_kind` (query, "user" | "api_key" | "oauth" | "system" | "orchestration" | "unknown") — Only entries made by this kind of actor. - `action` (query, "created" | "updated" | "deleted" | "event") — Only entries with this action. - `request_id` (query, string) — Only the entries one request produced. - `since` (query, string (date-time)) — Only entries strictly after this instant (ISO 8601 with offset), compared to the millisecond. - `until` (query, string (date-time)) — Only entries strictly before this instant (ISO 8601 with offset), compared to the millisecond. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `AuditEntryList` - `data` (array of AuditEntry, required) — Newest first (by `occurred_at`, then id). - `id` (string (uuid), required) - `occurred_at` (string (date-time), required) - `action` ("created" | "updated" | "deleted" | "event", required) — `created`, `updated`, `deleted` — the change itself. `event` — what a change MEANT (an asset created through the API, a file attached, a legal hold placed), recorded beside the change in the same request: its `changes` are on the entry with the same `request_id`. - `resource` (object, required) - `type` (string, required) — The kind of thing changed: `asset`, `file`, `project`, `collection`, `playlist`, `record`, `tag`, `tag_link`, `asset_type`, `field_definition`, `workflow`, `spec_profile`, `api_key`, `run`, `receipt`, `delivery_requirement`, `delivery_target`, `schedule_slot`, `service`, `channel`, `share_link`, `comment`, `marker`, `confirmation`, `other`. `other` is something the feed does not describe (its name and changes are withheld). More types may be added. - `id` (string, required) — The changed row's id (for an asset, the version's id). - `name` (string or null, required) — Its name where the feed knows it; null otherwise. - `actor` (object, required) - `kind` ("user" | "api_key" | "oauth" | "system" | "orchestration" | "unknown", required) — `user` — a person in the app; `api_key` — an org API key; `oauth` — an app acting for a person; `system` — the platform itself (a migration or maintenance); `orchestration` — a workflow run; `unknown` — not recorded. - `id` (string (uuid) or null, required) — The person's principal id (`user`, `oauth`), the key's id (`api_key`, when the entry records it), or the run's id (`orchestration`, GET /api/v1/runs/{id}); null otherwise. - `name` (string or null, required) — The person's name, or the key's name. - `client_id` (string or null, required) — For `oauth`, the app's OAuth client_id when the entry records it. - `request_id` (string or null, required) — Shared by every entry one request produced; filter by it to see a whole change. - `event` (object or null, required) — For an entry that records a semantic event: its kind and its public ids and names (which keys depend on the kind). Null otherwise. - `kind` (string, required) — The event's kind, e.g. `api_asset_create`, `legal_hold`, `derivation`. More may be added. - `changes` (array of object, required) — The public fields that changed. Empty for an `event`, for `other`, and when nothing public changed. - `field` (string, required) — A public field of the resource. `fields` (an asset's or a record's field values) reports the changed KEYS only. - `from` (any, required) — The value before (null when created or unset); for `fields`, the changed keys that were present before. - `to` (any, required) — The value after (null when deleted or cleared); for `fields`, the changed keys present after. - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad filter, `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/audit-log/{id} Get one audit entry One entry, in the list's shape. 404 when the org does not have the audit log, and for an OAuth token whose person may not view it. Scopes: `audit:read` Parameters: - `id` (path, string (uuid), required) — The entry's id, from GET /api/v1/audit-log. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `AuditEntry`: One entry of the org's audit trail, projected: who changed what and when, naming the public fields that changed — never the stored row images. - `id` (string (uuid), required) - `occurred_at` (string (date-time), required) - `action` ("created" | "updated" | "deleted" | "event", required) — `created`, `updated`, `deleted` — the change itself. `event` — what a change MEANT (an asset created through the API, a file attached, a legal hold placed), recorded beside the change in the same request: its `changes` are on the entry with the same `request_id`. - `resource` (object, required) - `type` (string, required) — The kind of thing changed: `asset`, `file`, `project`, `collection`, `playlist`, `record`, `tag`, `tag_link`, `asset_type`, `field_definition`, `workflow`, `spec_profile`, `api_key`, `run`, `receipt`, `delivery_requirement`, `delivery_target`, `schedule_slot`, `service`, `channel`, `share_link`, `comment`, `marker`, `confirmation`, `other`. `other` is something the feed does not describe (its name and changes are withheld). More types may be added. - `id` (string, required) — The changed row's id (for an asset, the version's id). - `name` (string or null, required) — Its name where the feed knows it; null otherwise. - `actor` (object, required) - `kind` ("user" | "api_key" | "oauth" | "system" | "orchestration" | "unknown", required) — `user` — a person in the app; `api_key` — an org API key; `oauth` — an app acting for a person; `system` — the platform itself (a migration or maintenance); `orchestration` — a workflow run; `unknown` — not recorded. - `id` (string (uuid) or null, required) — The person's principal id (`user`, `oauth`), the key's id (`api_key`, when the entry records it), or the run's id (`orchestration`, GET /api/v1/runs/{id}); null otherwise. - `name` (string or null, required) — The person's name, or the key's name. - `client_id` (string or null, required) — For `oauth`, the app's OAuth client_id when the entry records it. - `request_id` (string or null, required) — Shared by every entry one request produced; filter by it to see a whole change. - `event` (object or null, required) — For an entry that records a semantic event: its kind and its public ids and names (which keys depend on the kind). Null otherwise. - `kind` (string, required) — The event's kind, e.g. `api_asset_create`, `legal_hold`, `derivation`. More may be added. - `changes` (array of object, required) — The public fields that changed. Empty for an `event`, for `other`, and when nothing public changed. - `field` (string, required) — A public field of the resource. `fields` (an asset's or a record's field values) reports the changed KEYS only. - `from` (any, required) — The value before (null when created or unset); for `fields`, the changed keys that were present before. - `to` (any, required) — The value after (null when deleted or cleared); for `fields`, the changed keys present after. Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Delivery requirements and readiness #### GET /api/v1/delivery-requirements List the org's delivery requirements Standing demands: content in a project's subtree must meet a pinned spec profile, for a client, delivered to a target. Poll with `updated_after` (exclusive, millisecond). For an OAuth token, its person needs the read-delivery permission (no platform-admin bypass); a key reads the whole org. 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Scopes: `delivery:read` Parameters: - `project_id` (query, string (uuid)) — Only the requirements bound to this project (not its ancestors' or descendants'). - `status` (query, "active" | "fulfilled" | "cancelled") — Only requirements with this status. - `updated_after` (query, string (date-time)) — Only requirements changed strictly after this instant (ISO 8601 with offset), compared to the millisecond. - `sort` (query, "-updated_at" | "updated_at" | "created_at") — Sort key; a leading `-` is descending. Default `-updated_at`. Ties break on id. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `DeliveryRequirementList` - `data` (array of DeliveryRequirement, required) — In the requested sort (default `-updated_at`), ties broken by id. - `id` (string (uuid), required) - `project` (object, required) — The project the requirement covers, with every sub-project under it. - `id` (string (uuid), required) - `name` (string, required) - `client` (string or null, required) — Who the content is owed to; null for an internal demand (a house standard). - `target` (object or null, required) — The delivery target the obligation is delivered to (GET /api/v1/delivery-targets); null for an internal demand or when the target is gone. - `id` (string (uuid), required) - `name` (string, required) - `spec_profile` (SpecProfileRef or null, required) — The spec revision the content must meet, pinned when the requirement was made. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `status` ("active" | "fulfilled" | "cancelled", required) — `active` — in force (readiness and the spec gate apply it); `fulfilled`; `cancelled`. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad filter, `sort`, `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/delivery-requirements/{id} Get one delivery requirement One requirement, in the list's shape. 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Scopes: `delivery:read` Parameters: - `id` (path, string (uuid), required) — The requirement's id, from GET /api/v1/delivery-requirements. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `DeliveryRequirement`: A standing demand: content in a project's subtree must meet a spec profile, for a client, delivered to a target. - `id` (string (uuid), required) - `project` (object, required) — The project the requirement covers, with every sub-project under it. - `id` (string (uuid), required) - `name` (string, required) - `client` (string or null, required) — Who the content is owed to; null for an internal demand (a house standard). - `target` (object or null, required) — The delivery target the obligation is delivered to (GET /api/v1/delivery-targets); null for an internal demand or when the target is gone. - `id` (string (uuid), required) - `name` (string, required) - `spec_profile` (SpecProfileRef or null, required) — The spec revision the content must meet, pinned when the requirement was made. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `status` ("active" | "fulfilled" | "cancelled", required) — `active` — in force (readiness and the spec gate apply it); `fulfilled`; `cancelled`. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/delivery-requirements/{id}/readiness Read a requirement's readiness Every asset in the requirement's project subtree (current versions, by asset id) against its pinned spec — the app's readiness matrix for this requirement, a page of assets at a time. A passing verdict on any of an asset's files counts (a conformed rendition). Reading it is a demand: for an ACTIVE requirement, an asset's current deliverable file that has no verdict for the spec yet is evaluated now (and stored), as the app does; a fulfilled or cancelled requirement is served from stored verdicts only. 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Scopes: `delivery:read` Parameters: - `id` (path, string (uuid), required) — The requirement's id, from GET /api/v1/delivery-requirements. - `limit` (query, integer) — Assets per page, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page. Response 200 — `DeliveryReadiness`: Every asset in the requirement's project subtree against its spec, as the app's readiness matrix shows it. - `requirement` (DeliveryRequirement, required) — A standing demand: content in a project's subtree must meet a spec profile, for a client, delivered to a target. - `id` (string (uuid), required) - `project` (object, required) — The project the requirement covers, with every sub-project under it. - `id` (string (uuid), required) - `name` (string, required) - `client` (string or null, required) — Who the content is owed to; null for an internal demand (a house standard). - `target` (object or null, required) — The delivery target the obligation is delivered to (GET /api/v1/delivery-targets); null for an internal demand or when the target is gone. - `id` (string (uuid), required) - `name` (string, required) - `spec_profile` (SpecProfileRef or null, required) — The spec revision the content must meet, pinned when the requirement was made. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `status` ("active" | "fulfilled" | "cancelled", required) — `active` — in force (readiness and the spec gate apply it); `fulfilled`; `cancelled`. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `assets` (array of ReadinessEntry, required) — By asset id. Paginated: an asset's verdict is computed for the page that lists it. - `asset` (object, required) — An asset in the requirement's project subtree (its current version). - `id` (string (uuid), required) - `name` (string, required) - `status` ("passed" | "failed" | "indeterminate" | "pending", required) — `passed` — a file of the asset meets the spec (a conformed rendition counts); `failed` — measured and out of spec; `indeterminate` — measured, no failure, but a demanded value could not be measured; `pending` — nothing to judge yet (no deliverable file, not yet measured, or a conform in flight). - `cell` (object, required) - `failures` (array of ReadinessFailure, required) — Why the asset fails the spec; empty unless `failed`. - `field` (string, required) — The spec field the file fails, as the validator names it (e.g. `frame_rate`, `loudness`). - `reason` (string, required) — What was measured and what the spec requires, in words. - `code` (string, required) — A stable code, e.g. `CONFORMANCE_FRAME_RATE`. - `severity` ("error" | "warning", required) - `correctability` ("correct" | "transcode" | "reject", required) — `correct` — fixable in place; `transcode` — fixable by a conform transcode; `reject` — needs a new upload. - `file` (object or null, required) — The file the verdict is about ({ id, name: its filename }); null when the asset has no deliverable file. - `id` (string (uuid), required) - `name` (string, required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page of assets; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/delivery-requirements/{id}/receipts List a requirement's receipts GET /delivery-receipts pre-filtered to the requirement (404 for an unknown one). 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Scopes: `delivery:read` Parameters: - `id` (path, string (uuid), required) — The requirement's id, from GET /api/v1/delivery-requirements. - `asset_id` (query, string (uuid)) — Only deliveries of this asset: any version's id or its root id matches every version's receipts. - `status` (query, "pending" | "delivered" | "failed" | "rejected") — Only receipts with this status. - `created_after` (query, string (date-time)) — Only receipts created strictly after this instant (ISO 8601 with offset), compared to the millisecond. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `DeliveryReceiptList` - `data` (array of DeliveryReceipt, required) — Newest first (by `created_at`, then id). - `id` (string (uuid), required) - `requirement` (object or null, required) — The requirement the delivery fulfils ({ id, name: its client, or `Internal` for an internal demand }); null for a social delivery or when the requirement is gone. - `id` (string (uuid), required) - `name` (string, required) - `asset` (object or null, required) — The asset delivered, by its root id (GET /api/v1/assets/{id} takes it); `name` is the delivered version's. Null when the asset is deleted (soft or hard). - `id` (string (uuid), required) - `name` (string, required) - `file` (object or null, required) — The file delivered ({ id, name: its filename }); null when not recorded. - `id` (string (uuid), required) - `name` (string, required) - `target` (object or null, required) — The delivery target; null for a social delivery or when the target is gone. - `id` (string (uuid), required) - `name` (string, required) - `spec_profile` (SpecProfileRef or null, required) — The spec revision the delivery was made against. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `client` (string, required) — Who it was delivered to (kept when the requirement is gone). - `status` ("pending" | "delivered" | "failed" | "rejected", required) — `pending` — in flight; `delivered`; `failed` (see `error`); `rejected` — the receiving org refused it (see `rejection_note`). - `error` (string or null, required) - `rejection_note` (string or null, required) — Why the receiving org rejected the delivery. - `attestation` (object or null, required) — For a delivery to another org on the platform: what the supplier attested. Null otherwise. - `verdict` (string, required) — The supplier's conformance verdict on the file at delivery time (`ready` or `conformed`). - `sha256` (string or null, required) — The delivered file's SHA-256 (lowercase hex) as it landed; null until it has. - `created_at` (string (date-time), required) - `delivered_at` (string (date-time) or null, required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad filter, `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/delivery-requirements/{id}/deliver Deliver an asset against a requirement (guarded) A guarded action (ADR 0081): it moves content out of the org. The app's own deliver runs: the asset must be one of the org's, a file of it must pass the requirement's spec (`file_id` pins one; otherwise the best, a conformed rendition first), the destination (`target_id`, or the requirement's own) must be able to take it — for another org on the platform, an active supply link and a spec revision it still accepts. Then a Deliver run pushes the file and a receipt is opened in the org's ledger. The same file already in flight to the same destination for the requirement is 409 `delivery-in-flight` (`receipt_id` names it); an asset under legal hold is 409 `legal-hold`. **An API key's delivery starts at once**: 202 with the run and the receipt (poll either). **An OAuth token's does not**: 202 with a Confirmation naming the asset, the destination and the spec revision; once its person confirms it in impartire the delivery starts as them, and the confirmation's `result` is the Delivery. Its person needs the deliver permission in the org (asked first: 403 for any requirement id without it) and read on the asset. Audited: the receipt's capture and a `delivery.requested` event. Scopes: `delivery:write` Parameters: - `id` (path, string (uuid), required) — The requirement's id, from GET /api/v1/delivery-requirements. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `DeliverRequest` - `asset_id` (string (uuid), required) — The asset to deliver: its current version's id or its root id (the current version is delivered). It must be in the requirement's org; for an OAuth token, one its person may read. - `target_id` (string (uuid)) — Where to deliver it (GET /api/v1/delivery-targets). Omit to deliver to the requirement's own target. - `file_id` (string (uuid)) — The file to deliver: one of the asset's files whose verdict for the requirement's spec passes (GET /api/v1/delivery-requirements/{id}/readiness names it). Omit for the best passing file, a conformed rendition first. Response 202 — `Delivery`: A delivery that has started: `run` is the Deliver run that pushes the file (poll GET /api/v1/runs/{id} until its `status` leaves `active`), `receipt` its row in the org's ledger, `pending` until the push lands (`delivered`) or fails (`failed`) — re-read GET /api/v1/delivery-receipts/{id}. - `run` (Run, required) — A workflow run on an asset — one the API started, a person started in the app, or an upload set off. - `id` (string (uuid), required) - `workflow` (object, required) — The template version the run is pinned to (any version's id names its template in GET /api/v1/workflows/{id}). `name` is null when the run is `private`. - `id` (string (uuid), required) - `name` (string or null, required) - `asset` (object, required) — The asset the run is about. `name` is null for an OAuth token whose person can no longer read the asset (a run they started). - `id` (string (uuid), required) - `name` (string or null, required) - `status` ("active" | "completed" | "cancelled" | "failed", required) - `private` (boolean, required) — True when the run's template is private to a team the token's person is not in: the run's existence, status, team and times are shown, its workflow name, steps, current step, incident and params are not (always false for an API key). - `team` (object or null, required) — The team that owns the run, snapshotted from its template at start; null for an unowned run. - `id` (string (uuid), required) - `name` (string, required) - `parent_run_id` (string (uuid) or null, required) — For a sub-workflow run, the run that started it. - `started_at` (string (date-time), required) - `updated_at` (string (date-time), required) — Moves on every change of state — poll on it with GET /api/v1/runs?updated_after=. - `completed_at` (string (date-time) or null, required) - `started_via` (string, required) — api (started through this API) | manual (in the app) | trigger (by an arrival trigger, e.g. an upload). More may be added. - `current_step` (object or null, required) — The first open step at the run's current gate, while the run is active; null otherwise. - `id` (string (uuid), required) - `name` (string, required) - `status` (string, required) - `awaiting` ("approval" | "completion" | "automation" or null, required) — What the step waits on: `approval` (a person's verdict), `completion` (a person marking it done), `automation` (the pipeline — an automated step, a gate or a sub-workflow); null while it is parked on an incident. - `steps` (array of RunStep, required) - `id` (string (uuid), required) — The template step's id (of the template version the run is pinned to). - `name` (string, required) - `status` (string, required) — pending | in_progress | submitted | approved | completed | failed | skipped | incident (more may be added). - `incident` (object or null, required) — Set while an active run is parked on a step that failed and waits for an operator to retry it or fail the run (in the app). - `message` (string, required) - `params` (object (any keys) or null, required) — The run's launch parameters, as validated against the template's param_schema. - `receipt` (DeliveryReceipt, required) — One delivery attempt in the org's ledger. Append-only: a redelivery is a new receipt. - `id` (string (uuid), required) - `requirement` (object or null, required) — The requirement the delivery fulfils ({ id, name: its client, or `Internal` for an internal demand }); null for a social delivery or when the requirement is gone. - `id` (string (uuid), required) - `name` (string, required) - `asset` (object or null, required) — The asset delivered, by its root id (GET /api/v1/assets/{id} takes it); `name` is the delivered version's. Null when the asset is deleted (soft or hard). - `id` (string (uuid), required) - `name` (string, required) - `file` (object or null, required) — The file delivered ({ id, name: its filename }); null when not recorded. - `id` (string (uuid), required) - `name` (string, required) - `target` (object or null, required) — The delivery target; null for a social delivery or when the target is gone. - `id` (string (uuid), required) - `name` (string, required) - `spec_profile` (SpecProfileRef or null, required) — The spec revision the delivery was made against. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `client` (string, required) — Who it was delivered to (kept when the requirement is gone). - `status` ("pending" | "delivered" | "failed" | "rejected", required) — `pending` — in flight; `delivered`; `failed` (see `error`); `rejected` — the receiving org refused it (see `rejection_note`). - `error` (string or null, required) - `rejection_note` (string or null, required) — Why the receiving org rejected the delivery. - `attestation` (object or null, required) — For a delivery to another org on the platform: what the supplier attested. Null otherwise. - `verdict` (string, required) — The supplier's conformance verdict on the file at delivery time (`ready` or `conformed`). - `sha256` (string or null, required) — The delivered file's SHA-256 (lowercase hex) as it landed; null until it has. - `created_at` (string (date-time), required) - `delivered_at` (string (date-time) or null, required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person may not deliver in the org. - 404 (Problem) — not-found — the org does not have delivery, or the requirement is not one of the org's. - 409 (Problem) — delivery-in-flight (`receipt_id` and `run_id` name it); legal-hold; conflict — the requirement is not active, no file passes its spec, the destination is not ready or will not accept it; confirmation-pending; or idempotency-in-flight. - 422 (Problem) — unknown-asset, validation (a bad field, or `target_id` not a target of the org), or idempotency-mismatch (an OAuth token may not send Idempotency-Key). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 502 (Problem) — orchestration-unavailable — the delivery run did not start; nothing was delivered. ### Delivery receipts #### GET /api/v1/delivery-receipts List the org's delivery receipts The org's delivery ledger, newest first: one receipt per delivery attempt (a redelivery is a new receipt), to a target or to a social platform. A delivery to another org on the platform carries `attestation` (the supplier's verdict and, once landed, the file's SHA-256). Receipts have no `updated_at`; poll new ones with `created_after` and re-read a pending one by id. For an OAuth token, its person needs the read-delivery permission (no platform-admin bypass); a key reads the whole org. 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Scopes: `delivery:read` Parameters: - `asset_id` (query, string (uuid)) — Only deliveries of this asset: any version's id or its root id matches every version's receipts. - `requirement_id` (query, string (uuid)) — Only deliveries made for this requirement. - `status` (query, "pending" | "delivered" | "failed" | "rejected") — Only receipts with this status. - `created_after` (query, string (date-time)) — Only receipts created strictly after this instant (ISO 8601 with offset), compared to the millisecond. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `DeliveryReceiptList` - `data` (array of DeliveryReceipt, required) — Newest first (by `created_at`, then id). - `id` (string (uuid), required) - `requirement` (object or null, required) — The requirement the delivery fulfils ({ id, name: its client, or `Internal` for an internal demand }); null for a social delivery or when the requirement is gone. - `id` (string (uuid), required) - `name` (string, required) - `asset` (object or null, required) — The asset delivered, by its root id (GET /api/v1/assets/{id} takes it); `name` is the delivered version's. Null when the asset is deleted (soft or hard). - `id` (string (uuid), required) - `name` (string, required) - `file` (object or null, required) — The file delivered ({ id, name: its filename }); null when not recorded. - `id` (string (uuid), required) - `name` (string, required) - `target` (object or null, required) — The delivery target; null for a social delivery or when the target is gone. - `id` (string (uuid), required) - `name` (string, required) - `spec_profile` (SpecProfileRef or null, required) — The spec revision the delivery was made against. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `client` (string, required) — Who it was delivered to (kept when the requirement is gone). - `status` ("pending" | "delivered" | "failed" | "rejected", required) — `pending` — in flight; `delivered`; `failed` (see `error`); `rejected` — the receiving org refused it (see `rejection_note`). - `error` (string or null, required) - `rejection_note` (string or null, required) — Why the receiving org rejected the delivery. - `attestation` (object or null, required) — For a delivery to another org on the platform: what the supplier attested. Null otherwise. - `verdict` (string, required) — The supplier's conformance verdict on the file at delivery time (`ready` or `conformed`). - `sha256` (string or null, required) — The delivered file's SHA-256 (lowercase hex) as it landed; null until it has. - `created_at` (string (date-time), required) - `delivered_at` (string (date-time) or null, required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad filter, `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/delivery-receipts/{id} Get one delivery receipt One receipt, in the list's shape. 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Scopes: `delivery:read` Parameters: - `id` (path, string (uuid), required) — The receipt's id, from GET /api/v1/delivery-receipts. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `DeliveryReceipt`: One delivery attempt in the org's ledger. Append-only: a redelivery is a new receipt. - `id` (string (uuid), required) - `requirement` (object or null, required) — The requirement the delivery fulfils ({ id, name: its client, or `Internal` for an internal demand }); null for a social delivery or when the requirement is gone. - `id` (string (uuid), required) - `name` (string, required) - `asset` (object or null, required) — The asset delivered, by its root id (GET /api/v1/assets/{id} takes it); `name` is the delivered version's. Null when the asset is deleted (soft or hard). - `id` (string (uuid), required) - `name` (string, required) - `file` (object or null, required) — The file delivered ({ id, name: its filename }); null when not recorded. - `id` (string (uuid), required) - `name` (string, required) - `target` (object or null, required) — The delivery target; null for a social delivery or when the target is gone. - `id` (string (uuid), required) - `name` (string, required) - `spec_profile` (SpecProfileRef or null, required) — The spec revision the delivery was made against. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `client` (string, required) — Who it was delivered to (kept when the requirement is gone). - `status` ("pending" | "delivered" | "failed" | "rejected", required) — `pending` — in flight; `delivered`; `failed` (see `error`); `rejected` — the receiving org refused it (see `rejection_note`). - `error` (string or null, required) - `rejection_note` (string or null, required) — Why the receiving org rejected the delivery. - `attestation` (object or null, required) — For a delivery to another org on the platform: what the supplier attested. Null otherwise. - `verdict` (string, required) — The supplier's conformance verdict on the file at delivery time (`ready` or `conformed`). - `sha256` (string or null, required) — The delivered file's SHA-256 (lowercase hex) as it landed; null until it has. - `created_at` (string (date-time), required) - `delivered_at` (string (date-time) or null, required) Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/delivery-receipts/{id}/reject Reject a delivery made to the org (guarded) A guarded action (ADR 0081). The receiving org refuses a delivery another org made to it through a supply link (`{id}` from GET /inbound-deliveries): a `delivered` one becomes `rejected` with `rejection_note`, which the supplier reads on their receipt; the landed asset stays in your intake project. **An API key's rejection happens at once** (200). **An OAuth token's does not**: 202 with a Confirmation naming the asset and the supplier; its person, an org admin, confirms it in impartire. Audited: a `receipt.rejected` event in your org. 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Also 404 when the org does not have supply links. Scopes: `delivery:write` Parameters: - `id` (path, string (uuid), required) — The delivery's id, from GET /api/v1/inbound-deliveries (the supplier's receipt id). - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `RejectDeliveryRequest` - `rejection_note` (string, required) — Why your org refuses the delivery. Required: the supplier reads it on their receipt. Response 200 — `InboundDelivery`: A delivery another org made to yours through a supply link (the receiver's view of its receipt). - `id` (string (uuid), required) - `supplier_org` (object, required) — The org that delivered. - `id` (string (uuid), required) - `name` (string, required) - `asset_name` (string or null, required) — The supplier's asset name. - `spec_profile` (SpecProfileRef or null, required) — Your spec revision it was delivered against. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `status` ("pending" | "delivered" | "failed" | "rejected", required) - `received` (object or null, required) — Where the delivery landed in your org; null until it has. - `asset_id` (string (uuid) or null, required) — Your asset the delivery landed as (GET /api/v1/assets/{id}). - `file_id` (string (uuid) or null, required) — The landed primary file. - `sha256` (string or null, required) — The landed primary file's SHA-256 (lowercase hex). - `landed_at` (string or null, required) — When it landed (ISO 8601). - `redelivery` (boolean, required) — True when it landed as a new version of an asset an earlier delivery landed. - `rejection_note` (string or null, required) — Why you rejected it, when you did. - `error` (string or null, required) - `created_at` (string (date-time), required) - `delivered_at` (string (date-time) or null, required) Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person is not an org admin. - 404 (Problem) — not-found. - 409 (Problem) — conflict — the delivery is not `delivered`; confirmation-pending; or idempotency-in-flight. - 422 (Problem) — validation (`rejection_note` missing or empty), or idempotency-mismatch. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Inbound deliveries #### GET /api/v1/inbound-deliveries List deliveries made to the org The receiver's view: what other orgs delivered to yours through a supply link, newest first, with where each landed in your org. For an OAuth token, its person needs the read-delivery permission (no platform-admin bypass); a key reads the whole org. 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Also 404 when the org does not have supply links. Scopes: `delivery:read` Parameters: - `status` (query, "pending" | "delivered" | "failed" | "rejected") — Only deliveries with this status. - `created_after` (query, string (date-time)) — Only deliveries created strictly after this instant (ISO 8601 with offset), compared to the millisecond. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `InboundDeliveryList` - `data` (array of InboundDelivery, required) — Newest first (by `created_at`, then id). - `id` (string (uuid), required) - `supplier_org` (object, required) — The org that delivered. - `id` (string (uuid), required) - `name` (string, required) - `asset_name` (string or null, required) — The supplier's asset name. - `spec_profile` (SpecProfileRef or null, required) — Your spec revision it was delivered against. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `status` ("pending" | "delivered" | "failed" | "rejected", required) - `received` (object or null, required) — Where the delivery landed in your org; null until it has. - `asset_id` (string (uuid) or null, required) — Your asset the delivery landed as (GET /api/v1/assets/{id}). - `file_id` (string (uuid) or null, required) — The landed primary file. - `sha256` (string or null, required) — The landed primary file's SHA-256 (lowercase hex). - `landed_at` (string or null, required) — When it landed (ISO 8601). - `redelivery` (boolean, required) — True when it landed as a new version of an asset an earlier delivery landed. - `rejection_note` (string or null, required) — Why you rejected it, when you did. - `error` (string or null, required) - `created_at` (string (date-time), required) - `delivered_at` (string (date-time) or null, required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad filter, `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Spec profiles #### GET /api/v1/spec-profiles List the spec profiles the org may read Every technical delivery spec revision your org may read: its own, those another org published to it, and those its requirements pin. Who a spec is published to is not shown. For an OAuth token, its person needs the read-delivery permission (no platform-admin bypass); a key reads the whole org. 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Scopes: `delivery:read` Parameters: - `updated_after` (query, string (date-time)) — Only specs changed strictly after this instant (ISO 8601 with offset), compared to the millisecond. - `sort` (query, "name" | "-updated_at" | "updated_at") — Sort key; a leading `-` is descending. Default `name`. Ties break on id. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `SpecProfileList` - `data` (array of SpecProfile, required) — In the requested sort (default `name`), ties broken by id. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) - `owner_org` (object, required) — The org that wrote the spec. - `id` (string (uuid), required) - `name` (string, required) - `mine` (boolean, required) — True when your org owns it. - `status` ("private" | "published" | "retired", required) — `private` — the owner's own, editable; `published` — offered to suppliers, immutable (it changes by a new revision); `retired` — still readable, no longer accepted for new requirements. - `supersedes_id` (string (uuid) or null, required) — The revision this one replaced. - `superseded_by_id` (string (uuid) or null, required) — The revision that replaced this one, when you can read it. - `effective_from` (string (date-time) or null, required) - `accepts_until` (string (date-time) or null, required) — After this instant the spec is no longer accepted for new requirements. - `frame_rate` (string, required) - `drop_frame` (boolean, required) - `resolution` (string or null, required) - `aspect_ratio` (string or null, required) - `audio_config` (string or null, required) - `color_space` (string or null, required) - `hdr` (string or null, required) - `closed_captions` (boolean or null, required) - `description` (string or null, required) - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad filter, `sort`, `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/spec-profiles/{id} Get one spec profile One spec revision, in the list's shape; 404 for one your org may not read. 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Scopes: `delivery:read` Parameters: - `id` (path, string (uuid), required) — The spec profile revision's id, from GET /api/v1/spec-profiles. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `SpecProfile`: A technical delivery spec your org may read: its own, one another org published to it, or one a requirement of yours pins. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) - `owner_org` (object, required) — The org that wrote the spec. - `id` (string (uuid), required) - `name` (string, required) - `mine` (boolean, required) — True when your org owns it. - `status` ("private" | "published" | "retired", required) — `private` — the owner's own, editable; `published` — offered to suppliers, immutable (it changes by a new revision); `retired` — still readable, no longer accepted for new requirements. - `supersedes_id` (string (uuid) or null, required) — The revision this one replaced. - `superseded_by_id` (string (uuid) or null, required) — The revision that replaced this one, when you can read it. - `effective_from` (string (date-time) or null, required) - `accepts_until` (string (date-time) or null, required) — After this instant the spec is no longer accepted for new requirements. - `frame_rate` (string, required) - `drop_frame` (boolean, required) - `resolution` (string or null, required) - `aspect_ratio` (string or null, required) - `audio_config` (string or null, required) - `color_space` (string or null, required) - `hdr` (string or null, required) - `closed_captions` (boolean or null, required) - `description` (string or null, required) - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Delivery targets #### GET /api/v1/delivery-targets List the org's delivery targets Where the org delivers to: an S3-compatible bucket outside the platform, or another org through a supply link (left out when the org does not have supply links). Never a target's transport configuration or credentials. For an OAuth token, its person needs the read-delivery permission (no platform-admin bypass); a key reads the whole org. 404 when the org does not have delivery, and for an OAuth token whose person may not read it. Scopes: `delivery:read` Parameters: - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page. Response 200 — `DeliveryTargetList` - `data` (array of DeliveryTarget, required) — Oldest first (by `created_at`, then id). - `id` (string (uuid), required) - `name` (string, required) - `kind` ("s3" | "org", required) — `s3` — an S3-compatible bucket outside the platform; `org` — another org on the platform, through a supply link. - `receiver_org` (object or null, required) — For `org`: the org it delivers to. Null for `s3`. - `id` (string (uuid), required) - `name` (string, required) - `transport_ready` (boolean, required) — Whether a delivery can be made to it now (an `s3` target whose transport is not attached yet cannot). - `accepts_spec_profile_ids` (array of string (uuid), required) — The specs it accepts (GET /api/v1/spec-profiles/{id}): for `org`, the receiver's published specs your org may still pin. - `created_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit` or `cursor`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### EPG services #### GET /api/v1/services List the org's broadcast services The org's linear broadcast services (EPG channels) — each with its transmission spec, its fallback format and the formats bound to it — by sort_order, as the app lists them. Returned whole, at most 1000. Poll with `updated_after` (exclusive, millisecond). For an OAuth token, its person needs membership of the org (no platform-admin bypass); a key reads the whole org. 404 when the org does not have the EPG, and for an OAuth token whose person is not a member of the org. Scopes: `epg:read` Parameters: - `updated_after` (query, string (date-time)) — Only services changed strictly after this instant (ISO 8601 with offset), compared to the millisecond. A format binding added or removed does not change a service's `updated_at`; its ETag moves. Response 200 — `ServiceList` - `data` (array of Service, required) — By sort_order, then creation, as the app lists them. Returned whole, at most 1000. - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `number` (integer or null, required) — The channel number viewers know it by, when it has one. - `color` (string, required) — The colour the app shows the service in (a CSS hex colour). - `sort_order` (integer, required) — Its place in the app's list of services (ascending). - `spec_profile` (SpecProfileRef or null, required) — The transmission spec its programmes are judged against (GET /api/v1/spec-profiles/{id}); null when none is set. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `default_format` (object or null, required) — The fallback clock (GET /api/v1/formats/{id}) for a day no bound format covers; null when none is set. - `id` (string (uuid), required) - `name` (string, required) - `formats` (array of object, required) — The formats bound to the service, each with the days it applies, by `starts_on` (unbounded first), then format name. - `format` (object, required) — A clock the service may use (GET /api/v1/formats/{id}). - `id` (string (uuid), required) - `name` (string, required) - `starts_on` (string (date) or null, required) — The first broadcast day it applies (YYYY-MM-DD); null — no start bound. - `ends_on` (string (date) or null, required) — The last broadcast day it applies (YYYY-MM-DD); null — no end bound. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `next_cursor` (string or null, required) — Always null: the list is returned whole. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `updated_after`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/services/{id} Get one broadcast service One service, in the list's shape. 404 when the org does not have the EPG, and for an OAuth token whose person is not a member of the org. Scopes: `epg:read` Parameters: - `id` (path, string (uuid), required) — The service's id, from GET /api/v1/services. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `Service`: A linear broadcast service (an EPG channel): its brand, transmission spec and the clocks its schedule is built on. - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `number` (integer or null, required) — The channel number viewers know it by, when it has one. - `color` (string, required) — The colour the app shows the service in (a CSS hex colour). - `sort_order` (integer, required) — Its place in the app's list of services (ascending). - `spec_profile` (SpecProfileRef or null, required) — The transmission spec its programmes are judged against (GET /api/v1/spec-profiles/{id}); null when none is set. - `id` (string (uuid), required) - `name` (string, required) - `revision` (integer, required) — The spec's revision: a published spec changes only by a new revision, which supersedes this one. - `default_format` (object or null, required) — The fallback clock (GET /api/v1/formats/{id}) for a day no bound format covers; null when none is set. - `id` (string (uuid), required) - `name` (string, required) - `formats` (array of object, required) — The formats bound to the service, each with the days it applies, by `starts_on` (unbounded first), then format name. - `format` (object, required) — A clock the service may use (GET /api/v1/formats/{id}). - `id` (string (uuid), required) - `name` (string, required) - `starts_on` (string (date) or null, required) — The first broadcast day it applies (YYYY-MM-DD); null — no start bound. - `ends_on` (string (date) or null, required) — The last broadcast day it applies (YYYY-MM-DD); null — no end bound. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### EPG schedules #### GET /api/v1/schedules List a service's schedule in a window The programme slots of one service that overlap [from, to) — at most 31 days — by start time. Each slot carries its readiness as last evaluated (reading does not re-evaluate it). Poll a window with `updated_after` (exclusive, millisecond); a deleted slot simply stops appearing. For an OAuth token, its person needs membership of the org (no platform-admin bypass); a key reads the whole org. 404 when the org does not have the EPG, and for an OAuth token whose person is not a member of the org. Also 404 for a service that is not the org's. Scopes: `epg:read` Parameters: - `service_id` (query, string (uuid), required) — The service whose schedule to read (GET /api/v1/services lists them). Required. - `from` (query, string (date-time), required) — The start of the window (ISO 8601 with offset). Required. - `to` (query, string (date-time), required) — The end of the window (ISO 8601 with offset), after `from` and at most 31 days after it. Required. Slots overlapping [from, to) are listed. - `updated_after` (query, string (date-time)) — Only slots changed strictly after this instant (ISO 8601 with offset), compared to the millisecond. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `ScheduleSlotList` - `data` (array of ScheduleSlot, required) — By `starts_at`, then id. - `id` (string (uuid), required) - `service` (object, required) — The service the programme airs on. - `id` (string (uuid), required) - `name` (string, required) - `title` (string, required) - `description` (string or null, required) - `starts_at` (string (date-time), required) - `ends_at` (string (date-time), required) - `asset` (object or null, required) — The asset scheduled, by its root id (GET /api/v1/assets/{id} takes it); `name` is the version picked when it was scheduled. Null for a rotation slot or when the asset is deleted or not in this org. - `id` (string (uuid), required) - `name` (string, required) - `file` (object or null, required) — The file that airs ({ id, name: its filename }), pinned when the asset was scheduled so a later upload cannot change what airs; null when none is pinned, or the pinned file is not an asset file of this org. - `id` (string (uuid), required) - `name` (string, required) - `source` (object or object or null, required) — Where a rotation slot draws its programme from — `{ project }` or `{ collection }`; null for a slot with no source, or one whose source is deleted or not in this org. - `project` (object, required) - `id` (string (uuid), required) - `name` (string or null, required) — The project's or collection's name. Null — for an OAuth token — when its person may not read it (a project they cannot open; a collection outside the projects they can open, or not shared with them): the id is the slot's own data, the name is not disclosed. - `format` (object or null, required) — The clock the slot was authored against (GET /api/v1/formats/{id}), frozen on the slot; null for a freeform slot. - `id` (string (uuid), required) - `name` (string, required) - `readiness` ("pending" | "ready" | "not_ready" | "unknown", required) — Whether what is scheduled meets the service's transmission spec, as last evaluated (when the slot is made, when its asset or service changes, and when its file is measured — not when you read it): `ready` — it meets the spec, or the service has no spec; `not_ready` — it fails the spec; `pending` — nothing to judge yet (no asset or no file), or a value the spec demands could not be measured; `unknown` — the file is not measured yet. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a missing or bad `service_id`, `from` or `to`, a window wider than allowed, a bad filter, `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/schedules Put a programme on a service's schedule (guarded) A guarded action (ADR 0081): it changes what airs. The app's own schedule write runs: every id the slot names must be the org's — the service, the format, the asset (by its current version's id or its root id), the source project or collection, and the pinned file must be a file of the asset (any version) — else 422 naming the field; the file that airs is pinned (`file_id`, or the asset's primary video); the slot's readiness is evaluated against the service's spec; the transmission log is re-derived and the on-air graphic re-pushed. An asset under legal hold is 409 `legal-hold`. **An API key's slot is made at once**: 201 with the slot and its ETag. **An OAuth token's is not**: 202 with a Confirmation naming the programme, the service, its airtime and what airs; once its person confirms it in impartire the slot is made as them (every check again), and the confirmation's `result` is the ScheduleSlot. Its person must be an org admin (the app's verb; no platform-admin bypass). Audited: the slot's capture and a `schedule.created` event. 404 when the org does not have the EPG, and for an OAuth token whose person is not a member of the org. Scopes: `schedules:write` Parameters: - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Request body — `CreateScheduleSlotRequest` - `service_id` (string (uuid), required) — The service (EPG channel) it airs on, from GET /api/v1/services. - `title` (string, required) - `description` (string or null) - `starts_at` (string (date-time), required) - `ends_at` (string (date-time), required) — After `starts_at`. - `asset_id` (string (uuid) or null) - `file_id` (string (uuid) or null) — Pin this file to air: a file of the asset (any of its versions). Needs `asset_id`. Omit to pin the asset's primary video. - `source_project_id` (string (uuid) or null) — A rotation slot's source: a live project of this org. Not with `source_collection_id`. - `source_collection_id` (string (uuid) or null) — A rotation slot's source: a live collection of this org, by its id or its root id. Not with `source_project_id`. - `format_id` (string (uuid) or null) — The clock the slot is authored against (GET /api/v1/formats), frozen on the slot. - `broadcast_day` (string (date)) — The broadcast day (YYYY-MM-DD) the slot belongs to, when it is not `starts_at`'s UTC date — a programme after midnight belongs to the day before. Stamped once; a change never re-stamps it. Response 201 — `ScheduleSlot`: A programme on a service's schedule: what airs, when, and whether it is ready to air. - `id` (string (uuid), required) - `service` (object, required) — The service the programme airs on. - `id` (string (uuid), required) - `name` (string, required) - `title` (string, required) - `description` (string or null, required) - `starts_at` (string (date-time), required) - `ends_at` (string (date-time), required) - `asset` (object or null, required) — The asset scheduled, by its root id (GET /api/v1/assets/{id} takes it); `name` is the version picked when it was scheduled. Null for a rotation slot or when the asset is deleted or not in this org. - `id` (string (uuid), required) - `name` (string, required) - `file` (object or null, required) — The file that airs ({ id, name: its filename }), pinned when the asset was scheduled so a later upload cannot change what airs; null when none is pinned, or the pinned file is not an asset file of this org. - `id` (string (uuid), required) - `name` (string, required) - `source` (object or object or null, required) — Where a rotation slot draws its programme from — `{ project }` or `{ collection }`; null for a slot with no source, or one whose source is deleted or not in this org. - `project` (object, required) - `id` (string (uuid), required) - `name` (string or null, required) — The project's or collection's name. Null — for an OAuth token — when its person may not read it (a project they cannot open; a collection outside the projects they can open, or not shared with them): the id is the slot's own data, the name is not disclosed. - `format` (object or null, required) — The clock the slot was authored against (GET /api/v1/formats/{id}), frozen on the slot; null for a freeform slot. - `id` (string (uuid), required) - `name` (string, required) - `readiness` ("pending" | "ready" | "not_ready" | "unknown", required) — Whether what is scheduled meets the service's transmission spec, as last evaluated (when the slot is made, when its asset or service changes, and when its file is measured — not when you read it): `ready` — it meets the spec, or the service has no spec; `not_ready` — it fails the spec; `pending` — nothing to judge yet (no asset or no file), or a value the spec demands could not be measured; `unknown` — the file is not measured yet. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person is not an org admin. - 404 (Problem) — not-found. - 409 (Problem) — legal-hold — the asset is held; confirmation-pending; or idempotency-in-flight. - 422 (Problem) — validation (a bad field; `service_id`, `format_id`, `file_id`, `source_project_id` or `source_collection_id` not the org's; `ends_at` not after `starts_at`; both sources; `file_id` without `asset_id`), unknown-asset (`asset_id`), or idempotency-mismatch (an OAuth token may not send Idempotency-Key). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/schedules/{id} Get one schedule slot One slot, in the list's shape. 404 when the org does not have the EPG, and for an OAuth token whose person is not a member of the org. Scopes: `epg:read` Parameters: - `id` (path, string (uuid), required) — The slot's id, from GET /api/v1/schedules. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `ScheduleSlot`: A programme on a service's schedule: what airs, when, and whether it is ready to air. - `id` (string (uuid), required) - `service` (object, required) — The service the programme airs on. - `id` (string (uuid), required) - `name` (string, required) - `title` (string, required) - `description` (string or null, required) - `starts_at` (string (date-time), required) - `ends_at` (string (date-time), required) - `asset` (object or null, required) — The asset scheduled, by its root id (GET /api/v1/assets/{id} takes it); `name` is the version picked when it was scheduled. Null for a rotation slot or when the asset is deleted or not in this org. - `id` (string (uuid), required) - `name` (string, required) - `file` (object or null, required) — The file that airs ({ id, name: its filename }), pinned when the asset was scheduled so a later upload cannot change what airs; null when none is pinned, or the pinned file is not an asset file of this org. - `id` (string (uuid), required) - `name` (string, required) - `source` (object or object or null, required) — Where a rotation slot draws its programme from — `{ project }` or `{ collection }`; null for a slot with no source, or one whose source is deleted or not in this org. - `project` (object, required) - `id` (string (uuid), required) - `name` (string or null, required) — The project's or collection's name. Null — for an OAuth token — when its person may not read it (a project they cannot open; a collection outside the projects they can open, or not shared with them): the id is the slot's own data, the name is not disclosed. - `format` (object or null, required) — The clock the slot was authored against (GET /api/v1/formats/{id}), frozen on the slot; null for a freeform slot. - `id` (string (uuid), required) - `name` (string, required) - `readiness` ("pending" | "ready" | "not_ready" | "unknown", required) — Whether what is scheduled meets the service's transmission spec, as last evaluated (when the slot is made, when its asset or service changes, and when its file is measured — not when you read it): `ready` — it meets the spec, or the service has no spec; `not_ready` — it fails the spec; `pending` — nothing to judge yet (no asset or no file), or a value the spec demands could not be measured; `unknown` — the file is not measured yet. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### PATCH /api/v1/schedules/{id} Move or change a schedule slot (guarded) A guarded action (ADR 0081): it changes what airs. If-Match is required: the slot's ETag from the last GET (or the create), or its bare `v`. Send only the fields to change. The app's own write runs, with the same org checks as the create (422 naming the field); a new asset re-pins the file that airs and re-evaluates readiness, as does a move to another service; the transmission log is re-derived for the old and the new airtime and the on-air graphic re-pushed. Naming an asset under legal hold, or changing only the pinned file of a slot whose asset is held, is 409 `legal-hold`. **An API key's change happens at once** (200 with the slot and its new ETag). **An OAuth token's does not**: 202 with a Confirmation saying what moves or changes, by name; on confirm it applies only if the slot is still the version the person was shown (else the confirmation's `problem` is a 412). Its person must be an org admin. Audited: the slot's capture and a `schedule.updated` event. 404 when the org does not have the EPG, and for an OAuth token whose person is not a member of the org. Scopes: `schedules:write` Parameters: - `id` (path, string (uuid), required) — The slot's id, from GET /api/v1/schedules. - `If-Match` (header, string, required) — The slot's ETag, or its bare version. Request body — `UpdateScheduleSlotRequest` - `service_id` (string (uuid)) — Move it to another service of this org. - `title` (string) - `description` (string or null) - `starts_at` (string (date-time)) - `ends_at` (string (date-time)) — After `starts_at` (the new one, or the slot's). - `asset_id` (string (uuid) or null) — Another asset to air (its current version's id or its root id; the file is re-pinned), or null to make it a rotation slot. - `file_id` (string (uuid) or null) — Pin this file to air: a file of the scheduled asset (any of its versions) — the one `asset_id` names, or the slot's. On a slot whose asset is under legal hold, a change of the file is 409 `legal-hold`. - `source_project_id` (string (uuid) or null) - `source_collection_id` (string (uuid) or null) — By its id or its root id. - `format_id` (string (uuid) or null) Response 200 — `ScheduleSlot`: A programme on a service's schedule: what airs, when, and whether it is ready to air. - `id` (string (uuid), required) - `service` (object, required) — The service the programme airs on. - `id` (string (uuid), required) - `name` (string, required) - `title` (string, required) - `description` (string or null, required) - `starts_at` (string (date-time), required) - `ends_at` (string (date-time), required) - `asset` (object or null, required) — The asset scheduled, by its root id (GET /api/v1/assets/{id} takes it); `name` is the version picked when it was scheduled. Null for a rotation slot or when the asset is deleted or not in this org. - `id` (string (uuid), required) - `name` (string, required) - `file` (object or null, required) — The file that airs ({ id, name: its filename }), pinned when the asset was scheduled so a later upload cannot change what airs; null when none is pinned, or the pinned file is not an asset file of this org. - `id` (string (uuid), required) - `name` (string, required) - `source` (object or object or null, required) — Where a rotation slot draws its programme from — `{ project }` or `{ collection }`; null for a slot with no source, or one whose source is deleted or not in this org. - `project` (object, required) - `id` (string (uuid), required) - `name` (string or null, required) — The project's or collection's name. Null — for an OAuth token — when its person may not read it (a project they cannot open; a collection outside the projects they can open, or not shared with them): the id is the slot's own data, the name is not disclosed. - `format` (object or null, required) — The clock the slot was authored against (GET /api/v1/formats/{id}), frozen on the slot; null for a freeform slot. - `id` (string (uuid), required) - `name` (string, required) - `readiness` ("pending" | "ready" | "not_ready" | "unknown", required) — Whether what is scheduled meets the service's transmission spec, as last evaluated (when the slot is made, when its asset or service changes, and when its file is measured — not when you read it): `ready` — it meets the spec, or the service has no spec; `not_ready` — it fails the spec; `pending` — nothing to judge yet (no asset or no file), or a value the spec demands could not be measured; `unknown` — the file is not measured yet. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person is not an org admin. - 404 (Problem) — not-found. - 409 (Problem) — legal-hold — the named asset is held, or only the file changes on a slot whose asset is held; or confirmation-pending. - 412 (Problem) — precondition-failed — If-Match is stale; `etag` in the body is the current one. - 422 (Problem) — validation (no field, a bad field, an id not the org's, `ends_at` not after `starts_at`, both sources, `file_id` on a slot with no asset), or unknown-asset (`asset_id`). - 428 (Problem) — precondition-required — no If-Match. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### DELETE /api/v1/schedules/{id} Remove a schedule slot (guarded) A guarded action (ADR 0081): it changes what airs. The slot leaves the schedule and its transmission log; the on-air graphic is re-pushed. No If-Match, and never refused for a legal hold — taking a programme off air is not held. **An API key's removal happens at once** (204). **An OAuth token's does not**: 202 with a Confirmation naming the programme, the service and its airtime; the confirmation's `result` is null. Its person must be an org admin. Audited: the slot's capture and a `schedule.deleted` event. 404 when the org does not have the EPG, and for an OAuth token whose person is not a member of the org. Scopes: `schedules:write` Parameters: - `id` (path, string (uuid), required) — The slot's id, from GET /api/v1/schedules. Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 204 — An API key's removal: the slot is gone. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person is not an org admin. - 404 (Problem) — not-found. - 409 (Problem) — confirmation-pending. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Transmission log #### GET /api/v1/transmission-log Read a service's planned transmission log The events of one service's transmission log that overlap [from, to) — at most 7 days — by start time: each slot's programme, and the breaks and IDs its format places inside it. This is the PLANNED log, derived from the schedule and re-derived whenever the schedule changes; it is not an as-run record (none exists yet). Events have no `updated_at`: re-read the window. For an OAuth token, its person needs membership of the org (no platform-admin bypass); a key reads the whole org. 404 when the org does not have the EPG, and for an OAuth token whose person is not a member of the org. Also 404 for a service that is not the org's. Scopes: `epg:read` Parameters: - `service_id` (query, string (uuid), required) — The service whose log to read (GET /api/v1/services lists them). Required. - `from` (query, string (date-time), required) — The start of the window (ISO 8601 with offset). Required. - `to` (query, string (date-time), required) — The end of the window (ISO 8601 with offset), after `from` and at most 7 days after it. Required. Events overlapping [from, to) are listed. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `TransmissionLog`: The planned transmission log of a service in a window, derived from its schedule and the slots' formats. Not an as-run record. - `data` (array of TransmissionEvent, required) — By `starts_at`, then id. - `id` (string (uuid), required) - `service` (object, required) — The service the event airs on. - `id` (string (uuid), required) - `name` (string, required) - `slot_id` (string (uuid) or null, required) — The schedule slot it is derived from (GET /api/v1/schedules/{id}). - `event_type` ("primary" | "secondary", required) — `primary` — the programme itself, one per slot; `secondary` — a break or an ID inside it, from the slot's format. - `secondary_kind` ("break" | "noncommercial_break" | "id" or null, required) — For a secondary event, the format element that produced it; null for a primary event. - `title` (string, required) - `starts_at` (string (date-time), required) - `ends_at` (string (date-time), required) - `file` (object or null, required) — For a primary event, the file that airs ({ id, name: its filename }); null for a break or an ID (their content is chosen later). - `id` (string (uuid), required) - `name` (string, required) - `format` (object or null, required) — The format the event was derived from. - `id` (string (uuid), required) - `name` (string, required) - `status` ("planned" | "pushed" | "aired", required) — `planned` — derived from the schedule and re-derived when it changes; `pushed` — handed to playout; `aired`. Today every event is `planned`. - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a missing or bad `service_id`, `from` or `to`, a window wider than allowed, a bad `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### EPG formats #### GET /api/v1/formats List the org's broadcast formats The org's clocks — the structure of a programme slot as segments, breaks and IDs with their durations — by name. Poll with `updated_after` (exclusive, millisecond). For an OAuth token, its person needs membership of the org (no platform-admin bypass); a key reads the whole org. 404 when the org does not have the EPG, and for an OAuth token whose person is not a member of the org. Scopes: `epg:read` Parameters: - `updated_after` (query, string (date-time)) — Only formats changed strictly after this instant (ISO 8601 with offset), compared to the millisecond. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `FormatList` - `data` (array of Format, required) — By name, then id. - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `elements` (array of FormatElement, required) — The clock, in running order, as stored: elements run back to back from a slot's start, and the slot's length is their sum. In the transmission log a slot's breaks and IDs are these elements offset from its start (cut at its end). - `type` ("segment" | "break" | "noncommercial_break" | "id", required) — `segment` — programme content; `break` — a commercial break; `noncommercial_break` — a promo or PSA break; `id` — a station ID. - `duration_ms` (integer, required) — How long the element runs, in milliseconds. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `updated_after`, `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/formats/{id} Get one broadcast format One format, in the list's shape. 404 when the org does not have the EPG, and for an OAuth token whose person is not a member of the org. Scopes: `epg:read` Parameters: - `id` (path, string (uuid), required) — The format's id, from GET /api/v1/formats. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `Format`: A broadcast format (a clock): the structure of a programme slot — its segments, breaks and IDs. - `id` (string (uuid), required) - `name` (string, required) - `description` (string or null, required) - `elements` (array of FormatElement, required) — The clock, in running order, as stored: elements run back to back from a slot's start, and the slot's length is their sum. In the transmission log a slot's breaks and IDs are these elements offset from its start (cut at its end). - `type` ("segment" | "break" | "noncommercial_break" | "id", required) — `segment` — programme content; `break` — a commercial break; `noncommercial_break` — a promo or PSA break; `id` — a station ID. - `duration_ms` (integer, required) — How long the element runs, in milliseconds. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Analytics #### GET /api/v1/analytics/summary Read consumption analytics over a range The numbers of the app's analytics dashboard: views, unique viewers, playback time and downloads over `range` reporting days ending today (the org's reporting day — its time zone and start hour — not a UTC calendar day), the same over the range before it, one point per day, the ten most viewed assets, and the device and audience breakdowns. `viewer_type` filters to one audience (`api` — developer-API downloads). Reading it first materializes every reporting day that has ended into the rollup, as the app's dashboard does (a write of derived data only). Channel audience is not served. For an OAuth token, its person needs membership of the org (no platform-admin bypass) and an asset's name is null when they may not read the asset; a key reads the whole org. 404 when the org does not have analytics, and for an OAuth token whose person is not a member of the org. Scopes: `analytics:read` Parameters: - `range` (query, "7d" | "30d" | "90d" | "365d") — How many reporting days, today included. Default `30d`. - `viewer_type` (query, "all" | "internal" | "consumer" | "embed" | "api") — Filter to one audience. Default `all`. Response 200 — `ConsumptionAnalytics`: How the org's content was consumed over a range: the numbers of the app's analytics dashboard. - `range` (object, required) - `from` (string (date), required) — The first reporting day of the range (YYYY-MM-DD). - `to` (string (date), required) — The last reporting day of the range (YYYY-MM-DD), today included. - `reporting_day` (object, required) — The org's reporting day: every day, range and bucket here is one of these, not a UTC calendar day. - `timezone` (string, required) — The org's reporting-day time zone (IANA). - `day_start_hour` (integer, required) — The hour (0–23, in that zone) a reporting day starts — a broadcaster's day may start at 06:00. - `viewer_type` ("all" | "internal" | "consumer" | "embed" | "api", required) — The audience the figures are filtered to (`all` — none). `by_viewer_type` ignores it. - `summary` (AnalyticsTotals, required) — Consumption totals over a range. - `total_views` (integer, required) — Views: a surface that renders the asset was opened (once per session and asset). - `unique_viewers` (integer, required) — Distinct viewers over the whole range — exact, not a sum of daily uniques. - `total_playback_ms` (integer, required) — Milliseconds of playback watched. - `total_downloads` (integer, required) — Downloads: the bytes were taken. - `previous_period` (object, required) — The same totals over the range of equal length ending the day before `range.from`. - `total_views` (integer, required) — Views: a surface that renders the asset was opened (once per session and asset). - `unique_viewers` (integer, required) — Distinct viewers over the whole range — exact, not a sum of daily uniques. - `total_playback_ms` (integer, required) — Milliseconds of playback watched. - `total_downloads` (integer, required) — Downloads: the bytes were taken. - `from` (string (date), required) - `to` (string (date), required) - `series` (array of object, required) — One point per reporting day of the range, oldest first, zero-filled. - `day` (string (date), required) - `views` (integer, required) - `unique_viewers` (integer, required) — Distinct viewers that day. - `watched_ms` (integer, required) - `downloads` (integer, required) - `top_assets` (array of object, required) — The ten most viewed assets of the range (every version of an asset counts toward it), most viewed first. - `asset` (object, required) - `id` (string (uuid), required) — The asset's current version id (GET /api/v1/assets/{id}). - `name` (string or null, required) — The asset's name. Null — for an OAuth token — when its person may not read the asset: the figures are org figures every member sees on the dashboard; the name is not disclosed. - `views` (integer, required) - `downloads` (integer, required) - `by_device` (array of object, required) — Views, downloads and playback sessions together, by device, largest first. - `device_type` (string, required) — `desktop`, `mobile`, `tablet` or `unknown`. - `count` (integer, required) - `by_viewer_type` (array of object, required) — Views, downloads and playback sessions together, by audience, largest first — over every audience, whatever `viewer_type` is. - `viewer_type` (string, required) — `internal` (a member), `consumer`, `embed` (a share link) or `api` (a developer-API download). - `count` (integer, required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `range` or `viewer_type` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/analytics/operations Read operations analytics over a range The numbers of the app's operations view over `range` reporting days ending today: workflow runs started, completed, failed and open, with durations; approvals and their latency; open incidents; delivery attempts, outcomes and latency; assets created, time to first approval, and the assets nobody opened. `workflows.active` and `incidents.active` are true now, not range figures. For an OAuth token, its person needs membership of the org (no platform-admin bypass) and an asset's name is null when they may not read the asset; a key reads the whole org. 404 when the org does not have analytics, and for an OAuth token whose person is not a member of the org. Scopes: `analytics:read` Parameters: - `range` (query, "7d" | "30d" | "90d" | "365d") — How many reporting days, today included. Default `30d`. Response 200 — `OperationsAnalytics`: How the DAM was operated over a range — runs, approvals, incidents, deliveries, assets: the numbers of the app's operations view. - `range` (object, required) - `from` (string (date), required) — The first reporting day of the range (YYYY-MM-DD). - `to` (string (date), required) — The last reporting day of the range (YYYY-MM-DD), today included. - `reporting_day` (object, required) — The org's reporting day: every day, range and bucket here is one of these, not a UTC calendar day. - `timezone` (string, required) — The org's reporting-day time zone (IANA). - `day_start_hour` (integer, required) — The hour (0–23, in that zone) a reporting day starts — a broadcaster's day may start at 06:00. - `workflows` (object, required) - `started` (integer, required) — Runs started in the range. - `completed` (integer, required) — Runs that completed in the range (dated by when they ended). - `failed` (integer, required) - `cancelled` (integer, required) - `active` (integer, required) — Runs open right now, whenever they started. - `duration_p50_ms` (integer or null, required) — Median start-to-completion time of the runs completed in the range; null when none. - `duration_p90_ms` (integer or null, required) - `approvals` (object, required) - `submitted` (integer, required) - `approved` (integer, required) - `latency_p50_ms` (integer or null, required) — Median submit-to-approve time of the approvals given in the range; null when none. - `latency_p90_ms` (integer or null, required) - `incidents` (object, required) - `active` (integer, required) — Run steps parked for an operator right now. - `deliveries` (object, required) - `attempted` (integer, required) — Deliveries attempted in the range; `delivered` and `failed` count how those turned out (the rest are pending). - `delivered` (integer, required) - `failed` (integer, required) - `success_rate` (number or null, required) — delivered / attempted, 0–1; null when nothing was attempted. - `latency_p50_ms` (integer or null, required) - `assets` (object, required) - `created` (integer, required) - `time_to_first_approval_p50_ms` (integer or null, required) — Median time from an asset's creation to its first approval, over first approvals given in the range. - `unconsumed` (object, required) - `count` (integer, required) — Assets that existed when the range opened and were not viewed, downloaded or played during it. - `sample` (array of object, required) — Up to ten of them, newest first. - `asset` (object, required) - `id` (string (uuid), required) — The asset's current version id (GET /api/v1/assets/{id}). - `name` (string or null, required) — The asset's name. Null — for an OAuth token — when its person may not read the asset: the figures are org figures every member sees on the dashboard; the name is not disclosed. - `created_at` (string (date-time), required) - `series` (array of object, required) — One point per reporting day of the range, oldest first, zero-filled. - `day` (string (date), required) - `workflows_completed` (integer, required) - `deliveries_delivered` (integer, required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `range`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/analytics/search Read search analytics over a range The numbers of the app's search view over `range` reporting days ending today: asset searches, those that found nothing, unique searchers, the same over the range before it, one point per day, and the top queries and top zero-result queries. For an OAuth token, its person needs membership of the org (no platform-admin bypass) and an asset's name is null when they may not read the asset; a key reads the whole org. 404 when the org does not have analytics, and for an OAuth token whose person is not a member of the org. Scopes: `analytics:read` Parameters: - `range` (query, "7d" | "30d" | "90d" | "365d") — How many reporting days, today included. Default `30d`. Response 200 — `SearchAnalytics`: What people searched the asset library for, and did not find, over a range: the numbers of the app's search view. - `range` (object, required) - `from` (string (date), required) — The first reporting day of the range (YYYY-MM-DD). - `to` (string (date), required) — The last reporting day of the range (YYYY-MM-DD), today included. - `reporting_day` (object, required) — The org's reporting day: every day, range and bucket here is one of these, not a UTC calendar day. - `timezone` (string, required) — The org's reporting-day time zone (IANA). - `day_start_hour` (integer, required) — The hour (0–23, in that zone) a reporting day starts — a broadcaster's day may start at 06:00. - `summary` (SearchTotals, required) — Asset-search totals over a range. - `searches` (integer, required) - `zero_result_searches` (integer, required) - `unique_searchers` (integer, required) - `previous_period` (object, required) — The same totals over the range of equal length ending the day before `range.from`. - `searches` (integer, required) - `zero_result_searches` (integer, required) - `unique_searchers` (integer, required) - `from` (string (date), required) - `to` (string (date), required) - `series` (array of object, required) — One point per reporting day, oldest first, zero-filled. - `day` (string (date), required) - `searches` (integer, required) - `zero_results` (integer, required) - `top_queries` (array of object, required) — The 20 most frequent queries. - `query` (string, required) - `searches` (integer, required) - `avg_results` (number, required) — Mean result count, one decimal. - `zero_result_queries` (array of object, required) — The 20 most frequent queries that found nothing. - `query` (string, required) - `searches` (integer, required) Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `range`. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Members #### GET /api/v1/members List the org's members The people who are members of the org, by name, each with their email, their org roles and their teams. Every credential sees emails: members see each other's in the app. For an OAuth token, its person needs membership of the org (no platform-admin bypass); a key reads the whole org. 404 when the org does not have organization settings, and for an OAuth token whose person is not a member. Scopes: `org:read` Parameters: - `q` (query, string) — Case-insensitive substring of the name or the email. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `MemberList` - `data` (array of Member, required) — By name, then id. - `id` (string (uuid), required) — The member's principal id in this org. - `name` (string, required) - `email` (string, required) — Served to every credential: members see each other's email in the app. - `roles` (array of object, required) — The org roles assigned to the member, by name. - `id` (string (uuid), required) - `name` (string, required) - `teams` (array of object, required) — The teams the member belongs to (archived teams left out), by name. - `id` (string (uuid), required) - `name` (string, required) - `joined_at` (string (date-time), required) — When the member joined the org. - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. ### Playout channels #### GET /api/v1/channels List the org's playout channels The org's playout channels as the DAM stores them — mode, the service each airs, whether it records and where — by name. Never a stream key, an ingest key or a destination, for any credential. Not the live on-air state: reading never calls the playout engine; that status is served once the orchestration tier owns the channel's convergence loop. Poll with `updated_after` (exclusive, millisecond). For an OAuth token, its person needs membership of the org (no platform-admin bypass); a key reads the whole org. 404 when the org does not have playout, and for an OAuth token whose person is not a member of the org. Scopes: `channels:read` Parameters: - `updated_after` (query, string (date-time)) — Only channels changed strictly after this instant (ISO 8601 with offset), compared to the millisecond. - `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100. - `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged. Response 200 — `ChannelList` - `data` (array of Channel, required) — By name, then id. - `id` (string (uuid), required) - `name` (string, required) - `mode` ("strict" | "relay", required) — `strict` — a scheduled playlist the playout engine plays out; `relay` — a live feed restreamed to platforms. - `service` (object or null, required) — The broadcast service (EPG channel, GET /api/v1/services/{id}) the channel airs; null when none is linked. - `id` (string (uuid), required) - `name` (string, required) - `recording` (object or null, required) — Whether the channel records, and where the recordings go; null when it does not record. - `kind` ("live_input", required) — What is recorded: `live_input` — a relay channel's incoming live feed, kept as a lossless master and registered as an asset. - `project` (object or null, required) — The project recordings are filed into; null when none is set or it is no longer a live project of the org (they file into the org's Unfiled project). - `id` (string (uuid), required) - `name` (string or null, required) — Null — for an OAuth token — when its person may not read the project. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) - `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 422 (Problem) — validation — a bad `updated_after`, `limit` or `cursor` (`errors[].path` names it). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### GET /api/v1/channels/{id} Get one playout channel One channel, in the list's shape. 404 when the org does not have playout, and for an OAuth token whose person is not a member of the org. Scopes: `channels:read` Parameters: - `id` (path, string (uuid), required) — The channel's id, from GET /api/v1/channels. - `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged. Response 200 — `Channel`: A playout channel's configuration as the DAM stores it — not its live on-air state, never a stream key or destination. - `id` (string (uuid), required) - `name` (string, required) - `mode` ("strict" | "relay", required) — `strict` — a scheduled playlist the playout engine plays out; `relay` — a live feed restreamed to platforms. - `service` (object or null, required) — The broadcast service (EPG channel, GET /api/v1/services/{id}) the channel airs; null when none is linked. - `id` (string (uuid), required) - `name` (string, required) - `recording` (object or null, required) — Whether the channel records, and where the recordings go; null when it does not record. - `kind` ("live_input", required) — What is recorded: `live_input` — a relay channel's incoming live feed, kept as a lossless master and registered as an asset. - `project` (object or null, required) — The project recordings are filed into; null when none is set or it is no longer a live project of the org (they file into the org's Unfiled project). - `id` (string (uuid), required) - `name` (string or null, required) — Null — for an OAuth token — when its person may not read the project. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Other responses: - 304 — Not modified — the ETag in If-None-Match is current. - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. #### POST /api/v1/channels/{id}/start Start a playout channel (guarded) A guarded action (ADR 0081): it puts a service on air. The app's own start runs through the playout engine: the stored configuration is pushed, the channel started, then the on-air graphic's schedule pushed (a graphic that fails leaves the channel on air without it). Only a `strict` channel starts through the engine; a relay channel is 409. **An API key's start happens at once**: 200 with the Channel — as the DAM stores it: the answer carries no engine status (read the audit entry, or the app). **An OAuth token's does not**: 202 with a Confirmation naming the channel and the service; once its person confirms it in impartire it runs as them. Its person must be an org admin (the app's verb; no platform-admin bypass). Audited: a `channel.started` event with the engine's state after the call. 404 when the org does not have playout, and for an OAuth token whose person is not a member of the org. Scopes: `channels:control` Parameters: - `id` (path, string (uuid), required) — The channel's id, from GET /api/v1/channels. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Response 200 — `Channel`: A playout channel's configuration as the DAM stores it — not its live on-air state, never a stream key or destination. - `id` (string (uuid), required) - `name` (string, required) - `mode` ("strict" | "relay", required) — `strict` — a scheduled playlist the playout engine plays out; `relay` — a live feed restreamed to platforms. - `service` (object or null, required) — The broadcast service (EPG channel, GET /api/v1/services/{id}) the channel airs; null when none is linked. - `id` (string (uuid), required) - `name` (string, required) - `recording` (object or null, required) — Whether the channel records, and where the recordings go; null when it does not record. - `kind` ("live_input", required) — What is recorded: `live_input` — a relay channel's incoming live feed, kept as a lossless master and registered as an asset. - `project` (object or null, required) — The project recordings are filed into; null when none is set or it is no longer a live project of the org (they file into the org's Unfiled project). - `id` (string (uuid), required) - `name` (string or null, required) — Null — for an OAuth token — when its person may not read the project. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person is not an org admin. - 404 (Problem) — not-found. - 409 (Problem) — conflict — a relay channel; confirmation-pending; or idempotency-in-flight. - 422 (Problem) — idempotency-mismatch (an OAuth token may not send Idempotency-Key). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 502 (Problem) — playout-unavailable — the engine did not start the channel; nothing changed on air. #### POST /api/v1/channels/{id}/stop Stop a playout channel (guarded) A guarded action (ADR 0081): it takes a service off air. The app's own stop runs through the playout engine. Only a `strict` channel stops through the engine; a relay channel is 409. **An API key's stop happens at once**: 200 with the Channel — as the DAM stores it: the answer carries no engine status (read the audit entry, or the app). **An OAuth token's does not**: 202 with a Confirmation naming the channel and the service; once its person confirms it in impartire it runs as them. Its person must be an org admin (the app's verb; no platform-admin bypass). Audited: a `channel.stopped` event with the engine's state after the call. 404 when the org does not have playout, and for an OAuth token whose person is not a member of the org. Scopes: `channels:control` Parameters: - `id` (path, string (uuid), required) — The channel's id, from GET /api/v1/channels. - `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters. Response 200 — `Channel`: A playout channel's configuration as the DAM stores it — not its live on-air state, never a stream key or destination. - `id` (string (uuid), required) - `name` (string, required) - `mode` ("strict" | "relay", required) — `strict` — a scheduled playlist the playout engine plays out; `relay` — a live feed restreamed to platforms. - `service` (object or null, required) — The broadcast service (EPG channel, GET /api/v1/services/{id}) the channel airs; null when none is linked. - `id` (string (uuid), required) - `name` (string, required) - `recording` (object or null, required) — Whether the channel records, and where the recordings go; null when it does not record. - `kind` ("live_input", required) — What is recorded: `live_input` — a relay channel's incoming live feed, kept as a lossless master and registered as an asset. - `project` (object or null, required) — The project recordings are filed into; null when none is set or it is no longer a live project of the org (they file into the org's Unfiled project). - `id` (string (uuid), required) - `name` (string or null, required) — Null — for an OAuth token — when its person may not read the project. - `created_at` (string (date-time), required) - `updated_at` (string (date-time), required) Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person is not an org admin. - 404 (Problem) — not-found. - 409 (Problem) — conflict — a relay channel; confirmation-pending; or idempotency-in-flight. - 422 (Problem) — idempotency-mismatch (an OAuth token may not send Idempotency-Key). - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal. - 502 (Problem) — playout-unavailable — the engine did not stop the channel; nothing changed on air. ### Confirmations #### GET /api/v1/confirmations/{id} Read a guarded action's confirmation A guarded action asked for with an OAuth token does not run: it answers 202 with a Confirmation and a Location here, and its person confirms or declines it in impartire at `confirm_url` within 15 minutes. Poll this until `status` is `executed` (then `result` is the action's answer, or `problem` why it failed) or `declined`. Any scope; readable only by the OAuth client and person that asked for it — another token's confirmation, a malformed id, and every API key (a key acts at once, so it never has one) are 404. Scopes: any valid credential Parameters: - `id` (path, string (uuid), required) — The confirmation's id, from the 202 that created it (also its Location). Response 200 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired. - `id` (string (uuid), required) - `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire). - `action` (string, required) — The guarded action, e.g. `run.signal`. - `resource` (object, required) - `type` (string, required) — The kind of resource the action is on, e.g. `run`. - `id` (string (uuid) or null, required) - `name` (string, required) — The resource as the person was shown it when the action was asked for. - `summary` (string, required) — The action in words, as the person reads it on the confirmation page. - `client_id` (string, required) — The OAuth client that asked. - `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm. - `created_at` (string (date-time), required) - `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired. - `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending. - `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise. - `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise. - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/. Branch on this. - `title` (string, required) - `status` (integer, required) - `detail` (string, required) — What went wrong and what to do about it, written for the caller. - `instance` (string, required) — The request path. - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem. - `errors` (array of object) — On 422 validation: one entry per invalid field. - `path` (string, required) — The field or parameter, as a dotted path. - `message` (string, required) - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq. - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag. - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist). - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version. - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}). - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire. Other responses: - 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one. - 403 (Problem) — insufficient-scope — the key lacks a required scope. - 404 (Problem) — not-found. - 410 (Problem) — confirmation-expired — nobody decided within 15 minutes; the action did not run and never will. - 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds. - 500 (Problem) — internal.