A source group is the unit a composition draws on. One group is usually one recording - a webinar, an interview, a shoot - and its files are the angles and audio tracks of that recording. Groups are persistent: ingest a recording once and every future brief can draw on it, and a single cut can span up to four groups. Once a group is ready, its transcript is queryable and its footage is addressable by timecode.
Two ways in#
POST/v1/source-groups
One call creates the group and all of its files. Each file takes either a filename or a source_url - one or the other, never both, and never neither.
| Field | Type | Description |
|---|---|---|
name | string | Required. What the group is called in the dashboard and in listings. |
files | array | Required. One to eight file objects. More than eight is 422 invalid_request. How many of them may carry the camera role is a plan entitlement - one on Creator and Pro, two on Studio, four on Enterprise - and more than that is 402 multicam_not_entitled where enforcement is on. |
files[].filename | string | The upload path. The extension decides how the file is stored, and the response carries a signed upload URL for it. Mutually exclusive with source_url. |
files[].source_url | string | The pull path. An HTTPS URL the platform fetches for you. Mutually exclusive with filename. |
files[].role | string | camera (default) or audio. Camera files become the angles the edit can cut between; an audio file is a separate recording of the same event. |
files[].label | string | Optional. Your own name for the file - defaults to file-1, file-2 and so on. |
Files in one group may mix the two paths. A group is only ready when every file in it has finished, whichever way it arrived.
Path A - upload the bytes
Name the file and the response carries a signed upload URL for it. Three calls: create, PUT, confirm.
curl -sS https://anjin-media-api.fly.dev/v1/source-groups \
-H "authorization: Bearer $ANJIN_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Q3 product webinar",
"files": [
{ "filename": "q3-product.mp4", "role": "camera" }
]
}'{
"id": "9f4b1d2e-6c3a-4d51-8f0b-2a7c5e91d340",
"files": [
{
"id": "3a91c7e8-51bd-4f02-9c6a-8e2d40b7f915",
"upload": {
"url": "https://<storage-host>/<signed-upload-path>?token=<token>",
"token": "<token>",
"key": "5d2f7a10-8b3e-4c96-a1d7-6e0b4f38c215/9f4b1d2e-6c3a-4d51-8f0b-2a7c5e91d340/3a91c7e8-51bd-4f02-9c6a-8e2d40b7f915/original.mp4"
}
}
],
"minutes_plan": "plan_creator",
"minutes_remaining": 150,
"low_minutes": false
}Treat upload.url as opaque and PUT the bytes to it exactly as returned. token and key come back alongside it for clients that address storage directly - the object key is deterministic, derived from your account, the group, the file and the extension.
UPLOAD_URL="https://<storage-host>/<signed-upload-path>?token=<token>" curl -sS -X PUT "$UPLOAD_URL" \ -H "content-type: video/mp4" \ --data-binary @q3-product.mp4
Then confirm. This is the call that starts the work, so nothing is queued until you make it:
POST/v1/source-groups/:id/files/:fileId/complete
GROUP_ID=9f4b1d2e-6c3a-4d51-8f0b-2a7c5e91d340 FILE_ID=3a91c7e8-51bd-4f02-9c6a-8e2d40b7f915 curl -sS -X POST \ "https://anjin-media-api.fly.dev/v1/source-groups/$GROUP_ID/files/$FILE_ID/complete" \ -H "authorization: Bearer $ANJIN_API_KEY"
{
"status": "queued",
"minutes_plan": "plan_creator",
"minutes_remaining": 150,
"low_minutes": false
}{
"type": "https://anjin.media/errors/upload_incomplete",
"title": "Upload Incomplete",
"status": 409,
"detail": "object not found in storage",
"code": "upload_incomplete"
}Path B - hand over a URL
Give a file a source_url instead and the platform fetches it. There is no PUT and no confirmation call: the group goes to ingesting the moment it is created. This is the fastest way in when the footage is already somewhere you can sign a URL for.
curl -sS https://anjin-media-api.fly.dev/v1/source-groups \
-H "authorization: Bearer $ANJIN_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Q3 product webinar",
"files": [
{
"source_url": "https://files.example.com/webinars/q3-product.mp4",
"role": "camera"
}
]
}'The URL has to satisfy three conditions:
- HTTPS only. Plain HTTP is refused.
- Publicly resolvable. The hostname is resolved at request time and refused if it points at a private or loopback address.
- Direct. Redirects are not followed - the check ran against the URL you gave, so a
Locationheader would walk straight past it. Serve the bytes at the URL itself.
A URL that fails any of the first two is refused at request time, before any row is written:
{
"type": "https://anjin.media/errors/ssrf_blocked",
"title": "Ssrf Blocked",
"status": 422,
"detail": "source_url must use https",
"code": "ssrf_blocked"
}No credentials are sent with the fetch, so use a pre-signed URL if the object is not public. Signed URLs expire - make sure yours outlives the download.
Several files in one group
Up to eight files per group, each a camera angle or an audio track. How many camera files one group may hold is a plan entitlement - one on Creator and Pro, two on Studio, four on Enterprise - and every camera file's minutes count towards the allowance. Multi-camera groups are synchronised during perception and given angle letters from A in the order they were given, and the planner may cut between them:
{
"name": "Panel - three cameras and a lapel mix",
"files": [
{ "filename": "cam-wide.mp4", "role": "camera", "label": "wide" },
{ "filename": "cam-left.mp4", "role": "camera", "label": "left" },
{ "filename": "cam-right.mp4", "role": "camera", "label": "right" },
{ "filename": "board-mix.wav", "role": "audio", "label": "lapel mix" }
]
}Formats and limits#
Seven extensions are accepted:
mp4 · mov · mkv · wav · mp3 · m4a · aac
Anything else on a filename is refused at request time with 422 unsupported_extension, and the detail names the extension it rejected. A source_url is not extension-checked when you post it - the file is fetched and probed instead, so an undecodable source fails during ingest rather than at the create call.
Per file, the ceilings are:
- 16 GB of bytes.
- 4 hours of duration.
- 4K on the short edge - 2160 pixels, whichever way the frame is oriented.
These are enforced during ingest rather than at the create call, because two of the three are only knowable once the file has been probed. A file over any of them fails, and its group fails with it.
A single cut can draw on up to four groups. See Creating a composition for what happens once your footage is ready.
Ingest statuses#
A group walks four states on the way to being usable, and a fifth if something goes wrong:
| Status | Kind | What it means |
|---|---|---|
uploading | start | The group exists and is waiting for bytes. A group whose files all came from a source_url never sits here. |
ingesting | working | At least one file is being fetched, probed and transcoded. A mezzanine and an audio track are produced per file. |
perceiving | working | Every file is transcoded. The group is now being transcribed with word-level timing, diarized into speakers, and - for a multi-camera group - synchronised across angles. |
ready | terminal | Usable in a cut. duration_ms is filled in and source.ready fires. |
failed | terminal | Ingest or perception did not complete. The group carries an error and source.failed fires. |
Each file has its own status too - pending, uploaded, ingesting, ready or failed - and the group moves on to perception only once every one of them is ready.
GET/v1/source-groups/:id
{
"id": "9f4b1d2e-6c3a-4d51-8f0b-2a7c5e91d340",
"name": "Q3 product webinar",
"status": "perceiving",
"asr_status": "processing",
"vad_status": "complete",
"sync_status": "skipped",
"duration_ms": null,
"camera_count": 1,
"total_duration_s": 3541.2,
"source_files": [
{
"id": "3a91c7e8-51bd-4f02-9c6a-8e2d40b7f915",
"role": "camera",
"label": "file-1",
"status": "ready"
}
]
}Three fields report the perception stages separately, so a group sitting in perceiving tells you which one it is waiting on: asr_status (transcription and diarization), vad_status (speech and silence detection) and sync_status - pending, complete, failed or skipped. A group with two or more camera files is held out of ready until its sync_status is complete; a single-camera group never runs the stage at all and stays skipped. Rather than polling, register an endpoint for source.ready and source.failed - see Webhooks.
Deleting a group
DELETE/v1/source-groups/:id
Deletion needs the admin scope. It removes the group and its files and purges the stored bytes - originals, mezzanines, audio tracks and proxies alike.
curl -sS -X DELETE \ "https://anjin-media-api.fly.dev/v1/source-groups/$GROUP_ID" \ -H "authorization: Bearer $ANJIN_API_KEY"
{ "status": "deleted" }Only ingesting and perceiving refuse: a worker is holding those files, and pulling them out from under it would strand the job. Wait for the group to reach ready or failed, then delete. A group still uploading deletes cleanly - nothing is running against it yet - and any bytes already PUT to a signed URL but never confirmed are purged with it, so an abandoned upload does not linger in storage.
{
"type": "https://anjin.media/errors/conflict",
"title": "Conflict",
"status": 409,
"detail": "group is ingesting or perceiving - wait for it to finish or fail",
"code": "conflict"
}Transcripts#
GET/v1/source-groups/:id/transcript
Perception produces a word-timed, speaker-labelled transcript of the group. It is what the planner reads, and you can read it too - for search, for captions, or to check what is actually in an archive before briefing against it.
curl -sS \ "https://anjin-media-api.fly.dev/v1/source-groups/$GROUP_ID/transcript?format=segments" \ -H "authorization: Bearer $ANJIN_API_KEY"
Four formats, chosen with ?format=. Anything else is 422 invalid_format:
| format | Returns | What you get |
|---|---|---|
segments | JSON · default | Speaker-labelled passages with millisecond bounds and the word range each one spans. This is what the planner reads. |
words | JSON | Every word with its own timing, confidence, speaker label and a filler flag, plus the detected language. |
srt | text | SubRip captions for the whole source, returned as plain text. |
vtt | text | WebVTT captions for the whole source, returned as plain text. |
{
"segments": [
{
"idx": 412,
"speaker": "B",
"start_ms": 1412000,
"end_ms": 1425600,
"text": "So the question we get every single time is: where does our footage actually sit?",
"word_start_idx": 3980,
"word_end_idx": 3995
}
]
}{
"language": "en",
"words": [
{
"idx": 3980,
"text": "So",
"start_ms": 1412000,
"end_ms": 1412140,
"confidence": 0.98,
"speaker": "B",
"is_filler": false
}
]
}1 00:00:04,120 --> 00:00:08,940 Right, let us get started - thanks for joining. 2 00:00:09,180 --> 00:00:14,600 Today is about what shipped in Q3 and what that means for your team.
Speaker labels are diarization labels - A, B and so on - not names. Give them display names with PATCH /v1/source-groups/:id/speakers and the planner will use those names when it writes a cut plan.
The transcript is only available once transcription has finished. Ask earlier and the request tells you what stage it is at:
{
"type": "https://anjin.media/errors/not_ready",
"title": "Not Ready",
"status": 409,
"detail": "transcript is processing",
"code": "not_ready"
}With a ready group in hand, the quickstart takes you from here to a rendered file in five more calls; Authentication covers the scopes each call needs, and Errors & limits covers every problem code named on this page. For what an archive of ready groups is worth once you have one, see Working with an archive.
