Everything a composition produces is readable. The plan is a document, not an opaque job; the renders name the plan version they came from; and each finished render ships captions, an edit decision list and a quality report alongside the video. That is what makes an edit auditable after the fact rather than just watchable.
Cut statuses#
Five values, and none of them is a queue position:
| Status | Kind | What it means |
|---|---|---|
planning | working | A planner run is in flight - the first one, or a revision. plan is null only before the first plan is committed; a revision leaves the previous version readable while it runs. This is the one status that refuses a revise or a manual edit. |
planned | resting | A plan exists and nothing is running. Read it, revise it, edit it or render it. cut.planned fires on the way in. |
rendering | working | A render batch has claimed the cut. It stays here until every render in the batch is terminal. |
complete | resting | Every render is finished and at least one of them succeeded. The cut can be revised or rendered again from here. |
failed | recoverable | Planning did not complete. The cut carries an error saying which stage failed and why, and cut.failed fires. Not a dead end: a revise moves it back to planning and tries again. |
The moves between them are worth knowing precisely.
- Planning settles itself. A planner run ends in
plannedorfailed, and each emits its webhook -cut.plannedorcut.failed- on the same code path as the status write. - A render batch claims the cut. Requesting renders moves the cut from
plannedorcompleteintorenderingas a single atomic step. Only one request can win that claim, so two concurrent batches cannot both start: the loser gets409 busywithcut is already rendering. - Whichever render finishes last settles the cut. While any render in the batch is still
queuedorprocessing, the cut staysrendering. Once none is, the cut becomescompleteif at least one render succeeded, and drops back toplannedif every one of them failed. Aspects can finish in any order and the answer is the same. - A revise moves the cut back to planning from anything except
planningitself. It does not cancel renders already in flight; those keep the plan version they were started on and still settle normally.
Polling any of this works, but subscribing is the right shape: Webhooks covers the four cut and render events.
Reading a cut#
GET/v1/cuts/:id
The cut row, plus the latest plan document verbatim and the version number it belongs to. Both are null only until the first plan is committed - after that there is always a plan to read.
CUT_ID=c7d2a35f-0e64-4b19-a8d3-1f5c9b027e64 curl -sS "https://anjin-media-api.fly.dev/v1/cuts/$CUT_ID" \ -H "authorization: Bearer $ANJIN_API_KEY"
{
"id": "c7d2a35f-0e64-4b19-a8d3-1f5c9b027e64",
"account_id": "5d2f7a10-8b3e-4c96-a1d7-6e0b4f38c215",
"prompt": "Cut a 60 second answer to the data residency question.",
"source_group_ids": ["9f4b1d2e-6c3a-4d51-8f0b-2a7c5e91d340"],
"settings": {
"targetDurationS": 60,
"durationTolerancePct": 15,
"minSegmentDurationS": 3,
"pace": "standard",
"chronology": "flexible",
"shrinkPauses": true,
"maxPauseMs": 1500,
"pauseFloorMs": 350,
"snapWindowMs": 400,
"minSilenceMs": 150,
"plannerModelTier": "default",
"autoRender": false,
"aspects": ["9:16"],
"economy": false,
"fillerRemoval": false,
"cropMode": "bars"
},
"status": "planned",
"brand_kit_id": null,
"error": null,
"created_at": "2026-08-17T14:06:11.204Z",
"updated_at": "2026-08-17T14:07:02.881Z",
"plan": {
"version": 2,
"estimated_duration_s": 58.4,
"title": "Where your data actually lives",
"rationale": "Opens on the objection as asked, answers it, then lands the worked example.",
"warnings": [],
"settings": {
"targetDurationS": 60,
"durationTolerancePct": 15,
"pace": "standard",
"chronology": "flexible"
},
"entries": [
{
"n": 1,
"role": "hook",
"source_group": "9f4b1d2e-6c3a-4d51-8f0b-2a7c5e91d340",
"camera": "A",
"source_start_ms": 1412000,
"source_end_ms": 1425600,
"text": "So the question we get every single time is: where does our footage actually sit?",
"speaker": "B",
"tightening": [],
"pinned": false
},
{
"n": 2,
"role": "body",
"source_group": "9f4b1d2e-6c3a-4d51-8f0b-2a7c5e91d340",
"camera": "B",
"source_start_ms": 1426400,
"source_end_ms": 1458900,
"text": "It sits in the region you pick, and it never leaves it.",
"speaker": "A",
"speaker_name": "Priya Raman",
"tightening": [
{ "op": "pause_shrink", "at_ms": 1441200, "removed_ms": 620 }
],
"pinned": true
}
]
},
"plan_version": 2
}error is normally null. On a failed cut it names the stage that failed and the message. It can also be set on a perfectly healthy planned cut with stage: "auto_render": that is the plan telling you it committed but its automatic render did not start. Read status first - error alone does not mean failure.
Listing cuts
GET/v1/cuts
A keyset-paginated list, newest first, carrying four fields per cut. Pass limit (default 50, max 100) and follow next_cursor until it comes back null. Treat the cursor as opaque.
curl -sS "https://anjin-media-api.fly.dev/v1/cuts?limit=2" \ -H "authorization: Bearer $ANJIN_API_KEY"
{
"data": [
{
"id": "c7d2a35f-0e64-4b19-a8d3-1f5c9b027e64",
"prompt": "Cut a 60 second answer to the data residency question.",
"status": "planned",
"created_at": "2026-08-17T14:06:11.204Z"
},
{
"id": "2b8e4f01-9d63-4a57-b1c0-8e5a37d64f92",
"prompt": "Ninety seconds on why the migration took nine months.",
"status": "complete",
"created_at": "2026-08-16T09:41:55.017Z"
}
],
"next_cursor": "MjAyNi0wOC0xNlQwOTo0MTo1NS4wMTdafDJiOGU0ZjAxLTlkNjMtNGE1Ny1iMWMwLThlNWEzN2Q2NGY5Mg"
}The plan document#
The plan is returned exactly as it was written - no reshaping between storage and response - so an agent, a dashboard and an audit all read the same bytes.
| Field | Type | Description |
|---|---|---|
version | integer | Which version of this cut's plan the document is. Starts at 1 and increases by one for every revision and every manual edit. |
estimated_duration_s | number | The predicted length of the finished edit - the sum of the entry spans minus everything their tightening ops remove. The QC report checks the rendered file against it. |
settings | object | The settings the plan was built with, in their stored camelCase form. Recorded on the document so a plan read months later still says what shaped it. |
title | string | A short title for the edit, written by the planner. |
rationale | string | One or two sentences on why this shape answers the brief. It is the editorial argument, and it is worth reading before you revise. |
warnings | string[] | Everything the planner wants you to know: a duration target it could not hit, selections dropped for being shorter than min_segment_duration_s, and manually edited once a person has changed the plan by hand. Empty is the normal case. |
entries | object[] | The edit itself, in running order. Fields below. |
Entries
Each entry is one continuous window of one camera angle of one source group:
| Field | Type | Description |
|---|---|---|
n | integer | Position in the edit, 1..N. It is a position, not an identity - an edit that removes or reorders entries renumbers all of them. |
role | hook | body | close | What this segment is doing: the opening, the middle, the landing. |
source_group | string (uuid) | Which source group the footage came from. In a cut spanning several groups this is what tells you which recording you are looking at. |
camera | string | The angle letter within that group - A to H. Single-camera groups are always A. |
source_start_ms | integer | Where in the source the segment starts, in milliseconds. |
source_end_ms | integer | Where it ends. This pair is the provenance record: every second of the output points back at a timecode in footage you supplied. |
text | string | What is said in that window, from the transcript. |
speaker | string | null | The diarization label - A, B and so on - or null where the segment has no attributed speaker. |
speaker_name | string · optional | The display name for that label, present only once you have named the speaker on the group. Absent rather than null when you have not. |
tightening | object[] | The edits made inside the segment: pause_shrink and, when you asked for it, filler_remove. Each carries at_ms and removed_ms, so the finished duration is derivable from the document alone. |
crosstalk_adjusted | object · optional | Present only when a boundary was moved off a collision with another speaker talking over it. It records which edge moved, where the boundary would otherwise have been, and the words that were dropped. |
pinned | boolean | Set by a manual edit. A pinned entry is one you have decided about, and a later revision is asked to leave it alone. |
Nothing in an entry is generated footage. The pair of timestamps is the whole provenance story: given the plan and the source group, you can find the exact frames the edit used. The transcript those text values come from is readable in full - Uploading video covers the transcript endpoint and its four formats.
Plan versions#
GET/v1/cuts/:id/plans/:version
Every plan a cut has ever had is kept. Versions start at 1 and go up by one each time the plan changes, whoever changed it:
| Origin | Written by | What it is |
|---|---|---|
llm | usually version 1 | The planner's first answer to the brief, written when the cut was created. |
revision | POST …/revise | The planner's answer to an instruction about the previous version. It saw its own prior entries when it wrote this one. |
manual_edit | PATCH …/plan | A person removed, reordered or pinned entries. No planner ran, and the document carries manually edited in its warnings. |
CUT_ID=c7d2a35f-0e64-4b19-a8d3-1f5c9b027e64 curl -sS "https://anjin-media-api.fly.dev/v1/cuts/$CUT_ID/plans/1" \ -H "authorization: Bearer $ANJIN_API_KEY"
The response is that version's plan document on its own - the same shape as the plan field above, with no cut row wrapped around it. A version that does not exist is 404 not_found, and a version that is not a positive integer is 422 invalid_request.
Renders name their version too, so a cut with several versions and several batches stays unambiguous. Pin plan_version when you request a render and a revision landing in between cannot render something you have not read.
Renders#
GET/v1/cuts/:id/renders
One render row per aspect, per batch. A three-aspect request produces three rows from one plan, each moving through its own statuses:
| Status | Kind | What it means |
|---|---|---|
queued | start | The row exists and a worker has not picked it up yet. |
processing | working | A worker is assembling it: pulling the angles it needs, cutting to the timeline, burning captions, mixing and encoding. |
complete | terminal | Finished. The row now carries duration_ms, a signed url and its sidecars, and render.completed has fired. |
failed | terminal | The render did not produce a file. The row carries an error naming the stage, and render.failed has fired. |
CUT_ID=c7d2a35f-0e64-4b19-a8d3-1f5c9b027e64 curl -sS "https://anjin-media-api.fly.dev/v1/cuts/$CUT_ID/renders" \ -H "authorization: Bearer $ANJIN_API_KEY"
{
"data": [
{
"id": "4b1f8c07-92ad-4e35-b6c1-7d0a3e58f214",
"aspect": "9:16",
"status": "complete",
"plan_version": 2,
"duration_ms": 58400,
"url": "https://<storage-host>/media-renders/<key>.mp4?X-Amz-Expires=3600&X-Amz-Signature=<sig>",
"sidecars": {
"srt": "https://<storage-host>/media-renders/<key>.srt?X-Amz-Signature=<sig>",
"edl": "https://<storage-host>/media-renders/<key>.edl.json?X-Amz-Signature=<sig>",
"qc": "https://<storage-host>/media-renders/<key>.qc.json?X-Amz-Signature=<sig>",
"ass": "https://<storage-host>/media-renders/<key>.ass?X-Amz-Signature=<sig>"
}
},
{
"id": "8e5c2a41-63f7-4d90-8b2e-05ca9146d738",
"aspect": "16:9",
"status": "processing",
"plan_version": 2,
"duration_ms": null
}
]
}The list is not paginated: it returns every render the cut has, across every batch and every re-render, oldest first. A render that is not complete carries no url and no sidecars at all - the keys are absent rather than null, so checking status is the reliable test.
Sidecars
A finished render is a video and the evidence for it. Four sidecars always, and a fifth whenever the edit has speech:
| Key | Format | What it is |
|---|---|---|
srt | text · always | SubRip captions timed to the finished edit, one cue per segment. Use these for a platform that wants its own caption track. |
vtt | text · always | The same cues as WebVTT - the format an HTML5 <track> element takes, for captioning the render in a web player without conversion. |
ass | text · when there is speech | The styled subtitle file that was burned into the picture, including whatever your brand kit specified. Present whenever the edit had caption cues to burn - a silent edit has no ass entry at all. |
edl | JSON · always | The edit decision list: the plan version, the assembled timeline, the caption cues and the crop decision made for each window. This is the file to hand a person who wants to rebuild the edit somewhere else. |
qc | JSON · always | The automated check on the finished file - duration against the plan, long black frames, loudness, decodability, captions, and the crop coverage checks when window machinery ran. |
{
"passed": true,
"checks": [
{ "name": "duration", "passed": true, "detail": "probed=58400ms expected=58400±1000" },
{ "name": "black_frames", "passed": true, "detail": "0 interval(s) ≥ 0.4s" },
{ "name": "loudness", "passed": true, "detail": "measured=-14.1 target=-14±1" },
{ "name": "decodable", "passed": true, "detail": "probe ok" },
{ "name": "captions", "passed": true, "detail": "expected=true burned=true" },
{ "name": "crop_coverage", "passed": true, "detail": "covered=58400ms timeline=58400ms windows=7" }
]
}qc_passed also travels in the render.completed webhook, so a pipeline can gate on it without downloading anything.
Deleting a render
DELETE/v1/renders/:id
Needs the admin scope. Only a terminal render can be deleted: one that is queued or processing is held by a worker and refuses with 409 conflict. Deleting purges the output and every sidecar from storage; the plan it came from is untouched, so the same version can be rendered again.
Usage on responses#
Responses that meter usage carry additive fields on top of their own body. On a published plan, an Enterprise contract or a live trial:
minutes_plan- the account's plan id,nullwhen it has none.minutes_remaining- source minutes left in the current period, to one decimal;nullwhen the allowance is custom. Rendering never moves it: source minutes are metered once, when footage is ingested.low_minutes-trueat ten percent of the allowance or less.
A legacy credit account gets credits_remaining and low_credits instead. All of it is informational. None of the fields gates a request, and if the allowance cannot be read they are simply omitted rather than the call failing - which is exactly why your own code should not treat their presence as guaranteed. Show them to a person; do not branch on them. For what the plans include and cost, see pricing; for the account-level usage totals, GET /v1/usage is documented in the API reference.
From here: Webhooks replaces the polling in every example above, Errors & limits covers every problem code named on this page, and MCP & agents exposes the same reads as agent tools.
