Developers
The impartire API
Upload files, create and edit assets, and read projects from your own server with a scoped API key — or let Claude, ChatGPT and other agents work for a person over MCP. The contract is openapi.json; a plain-text copy of these docs for agents is llms-full.txt.
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.
export IMPARTIRE_KEY=impk_…2. Check who you are
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
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
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.mp45. Create the asset, then read it back
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":"<type id>","project_id":"<project id>","name":"Promo","file":{"upload_id":"<upload_id>"}}'
curl -s https://impartire.com/api/v1/assets/<asset id> -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).
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":"<workflow id>","asset_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.
curl -s https://impartire.com/api/v1/runs/<run id> -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. 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.
curl -s -X POST https://impartire.com/api/v1/assets/<asset id>/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/<asset id>/tags/<tag id> -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 names the rule). The key is the new project's owner. Rename, describe or retype it with PATCH under the ETag the create returned.
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":"<project id>"}' # -i: the ETag header comes with the body
curl -s -X PATCH https://impartire.com/api/v1/projects/<new project id> -H "Authorization: Bearer $IMPARTIRE_KEY" \
-H "Content-Type: application/json" -H 'If-Match: "<etag>"' -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.
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":"<record type id>","title":"Harbour Line — distribution","fields":{"<reference field key>":"<asset id>"}}'
curl -s https://impartire.com/api/v1/assets/<asset id>/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.
curl -s "https://impartire.com/api/v1/assets/<asset id>/comments?resolved=false" -H "Authorization: Bearer $IMPARTIRE_KEY"
curl -s https://impartire.com/api/v1/assets/<asset id>/markers -H "Authorization: Bearer $IMPARTIRE_KEY"
curl -s https://impartire.com/api/v1/assets/<asset id>/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.
curl -s https://impartire.com/api/v1/assets/<asset id>/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/<person id>/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/<asset id>/categories -H "Authorization: Bearer $IMPARTIRE_KEY"
curl -s "https://impartire.com/api/v1/assets?category_id=<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.
curl -s https://impartire.com/api/v1/assets/<asset id>/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.
curl -s "https://impartire.com/api/v1/audit-log?resource_id=<asset version 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.
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/<requirement id>/readiness -H "Authorization: Bearer $IMPARTIRE_KEY"
curl -s "https://impartire.com/api/v1/delivery-receipts?asset_id=<asset id>" -H "Authorization: Bearer $IMPARTIRE_KEY"
curl -s https://impartire.com/api/v1/spec-profiles/<spec profile id> -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, naming that receipt; send an Idempotency-Key to retry a request safely. An asset under legal hold is 409 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.
curl -s -X POST https://impartire.com/api/v1/delivery-requirements/<requirement id>/deliver -H "Authorization: Bearer $IMPARTIRE_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{ "asset_id": "<asset id>" }'
# the receiving org, refusing what landed
curl -s -X POST https://impartire.com/api/v1/delivery-receipts/<delivery id>/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.
curl -s https://impartire.com/api/v1/services -H "Authorization: Bearer $IMPARTIRE_KEY"
curl -s "https://impartire.com/api/v1/schedules?service_id=<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=<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/<format id> -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; 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.
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": "<service id>", "title": "Evening News", "starts_at": "2026-10-30T18:00:00Z", "ends_at": "2026-10-30T18:30:00Z", "asset_id": "<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/<slot id> -H "Authorization: Bearer $IMPARTIRE_KEY" -H 'If-Match: "<etag>"' \
-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.
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 and nothing changed on air. A key acts at once; with an OAuth token its person, an org admin, confirms it in impartire.
curl -s -X POST https://impartire.com/api/v1/channels/<channel id>/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, …).
curl -s -X POST https://impartire.com/api/v1/projects/<project id>/access -H "Authorization: Bearer $IMPARTIRE_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{ "subject_type": "member", "subject_id": "<principal id>", "level": "viewer" }'
curl -s -X DELETE "https://impartire.com/api/v1/projects/<project id>/access?subject_type=member&subject_id=<principal 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), 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.
curl -s -X POST https://impartire.com/api/v1/assets/<asset id>/share-links -H "Authorization: Bearer $IMPARTIRE_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{ "expires_in_days": 7, "password": "<a password>", "allow_download": false }'
# → { …, "url": "https://impartire.com/share/…" } — shown onceThen: 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.
curl -s https://impartire.com/api/v1/assets/<asset id>/files -H "Authorization: Bearer $IMPARTIRE_KEY"
curl -si https://impartire.com/api/v1/assets/<asset id>/files/<file id> -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/<asset id>/files/<an earlier version's id>/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.
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.
curl -si -X POST https://impartire.com/api/v1/assets/<asset id>/files -H "Authorization: Bearer $IMPARTIRE_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"upload_id":"<upload_id>","replace_file_id":"<file id>","version_note":"Audio fix, QC 4412"}'
curl -s -X POST https://impartire.com/api/v1/assets/<asset id>/files/<an earlier version's id>/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. To give up, POST /uploads/{id}/abort frees the stored parts. The MCP tool impartire_start_upload does single PUTs only.
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/<upload_id>/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/<upload_id>/complete -H "Authorization: Bearer $IMPARTIRE_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"parts":[{"part_number":1,"etag":"<etag>"},{"part_number":2,"etag":"<etag>"},{"part_number":3,"etag":"<etag>"}]}'Authentication
Every request except the OpenAPI document 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. <resource>:write includes <resource>: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/mcpand/.well-known/oauth-protected-resource/api/v1— the two resources (RFC 9728)/.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: withruns:signalit may only cancel a run. - An OAuth token's guarded action never runs when it is asked for: the endpoint answers
202 Acceptedwith aConfirmation, 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; asking for the same action again while one waits answers 409 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/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
claude mcp add --transport http impartire https://impartire.com/mcpClaude 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/<code>; 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 | 401 | Unauthenticated |
insufficient-scope | 403 | Insufficient scope |
insufficient-permission | 403 | Insufficient permission |
not-found | 404 | Not found |
validation | 422 | Validation failed |
idempotency-mismatch | 422 | Idempotency key reused |
idempotency-in-flight | 409 | Request in progress |
precondition-failed | 412 | Precondition failed |
precondition-required | 428 | Precondition required |
type-changed | 409 | Asset type changed |
rate-limited | 429 | Rate limited |
conflict | 409 | Conflict |
legal-hold | 409 | Under legal hold |
unknown-workflow | 422 | Unknown workflow |
unknown-asset | 422 | Unknown asset |
unknown-tag | 422 | Unknown tag |
invalid-hierarchy | 422 | Invalid project hierarchy |
collection-constraint | 422 | Collection constraint |
already-member | 409 | Already a member |
project-type-disallows | 422 | Project type disallows |
confirmation-required | 403 | Confirmation required |
confirmation-pending | 409 | Confirmation pending |
delivery-in-flight | 409 | Delivery in flight |
confirmation-expired | 410 | Confirmation expired |
orchestration-unavailable | 502 | Orchestration unavailable |
playout-unavailable | 502 | Playout unavailable |
already-current | 422 | Already current |
upload-not-landed | 422 | Upload not landed |
not-multipart | 422 | Not a multipart upload |
size-mismatch | 422 | Size mismatch |
storage-unavailable | 503 | Storage unavailable |
authorization-unavailable | 503 | Authorization unavailable |
authorization-write-failed | 500 | Authorization write failed |
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; a retry while the first is still running is 409 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; stale, 412 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.
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 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: @<unix-seconds> and Sunset: <HTTP date> 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 |
Reference
Every path is under https://impartire.com/api/v1. Generated from the API’s own contract, openapi.json (OpenAPI 3.1), which you can load into any OpenAPI tool. Errors are Problems — see Conventions.
One page per resource, every field expanded; the reference index lists every operation on one page.
- The contract · 1 operation
- Who is calling · 1 operation
- Asset types · 1 operation
- Projects · 13 operations
- Uploads · 5 operations
- Assets · 10 operations
- Asset files · 5 operations
- Asset comments and markers · 7 operations
- Asset share links · 3 operations
- Asset tags · 3 operations
- Workflows · 2 operations
- Runs · 4 operations
- Tags · 1 operation
- Collections · 10 operations
- Playlists · 9 operations
- Record types · 1 operation
- Records · 5 operations
- People and credits · 3 operations
- Categories · 1 operation
- Audit log · 2 operations
- Delivery requirements and readiness · 5 operations
- Delivery receipts · 3 operations
- Inbound deliveries · 1 operation
- Spec profiles · 2 operations
- Delivery targets · 1 operation
- EPG services · 2 operations
- EPG schedules · 5 operations
- Transmission log · 1 operation
- EPG formats · 2 operations
- Analytics · 3 operations
- Members · 1 operation
- Playout channels · 4 operations
- Confirmations · 1 operation