Studio
Upload or import media, run Studio operations, manage thumbnails and subtitles, and track media credits.
Studio takes your video files and turns them into processed media and metadata: a preview rendition, transcript, scene analysis, thumbnails, ad context, subtitles and dubs. Each file becomes a media item with one or more jobs. Studio results that describe a title (a transcript, for example) reach the Library as proposals for a person to review.
Rules the server enforces
- Operations spend media credits. Estimate first with
GET /v1/studio/estimateand check your balance withGET /v1/studio/credits. A job that can't hold enough credits stops with statusinsufficient_creditsand does no paid work. - Media files are at most 4.5 GiB. An upload is a single presigned
PUT, which is why. A larger file returns413 TOO_LARGE. - Every title lives in a catalog you choose. An upload that will become a new title needs
catalogIdornewCatalogName, and alibraryType. - Duplicate bytes never create a second title. A byte-identical upload attaches to the existing title.
- Studio must be on your plan. Without it, every Studio call returns
403 MODULE_NOT_ENABLED.
Check cost and balance
curl "https://dev.app.ingenee.ai/api/v1/studio/estimate?durationSec=5400&ops=transcribe_assemblyai,thumbnails" \
-H "Authorization: Bearer $INGENEE_API_KEY"{
"estimate": {
"durationSec": 5400,
"lines": [ { "operation": "thumbnails", "pool": "media_processing", "creditsPerMinute": 0.1, "credits": 9 } ],
"totals": { "media_processing": 9, "media_intelligence": 90 }
}
}durationSec is 1 to 86,400 and ops is a comma-separated list of operation names (see Operations). An unknown
name returns 422. An estimate is a quote: the binding hold is taken from the measured duration once the file has been probed.
Credits live in two pools, media_processing and media_intelligence. The rates and pools in this example are illustrative; your own come from GET /v1/studio/credits.
GET /v1/studio/credits returns balances for both pools, the rates (credits per minute for each operation, with usable
and an unavailableReason when an operation can't run for you), and the 50 newest ledger entries.
GET /v1/studio/credits/ledger pages the full ledger with page, pageSize, pool, kind, operation and sort (when or credits).
Upload a file
Create the job
Declare the file, where its title goes, and what to run.
curl -X POST https://dev.app.ingenee.ai/api/v1/studio/jobs \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{
"source": "upload",
"sourceRef": "the-long-night.mp4",
"contentType": "video/mp4",
"bytes": 2147483648,
"catalogId": "0e578f59-bf28-455f-bc82-f0639a7575fb",
"libraryType": "movie",
"operations": ["thumbnails", "local_analysis"]
}'{
"jobId": "a41c9e07-3b52-4d68-b1f0-7c2e5d8a9b13",
"mediaItemId": "e0b7d3a2-6f14-4c85-9a2d-1b3c5e7f9a04",
"uploadUrl": "https://…s3.amazonaws.com/…?X-Amz-Signature=…",
"key": "…"
}sourceRef is the file name. libraryType is movie, episode, series or song. Without catalogId/newCatalogName you get
422 CATALOG_REQUIRED, and without libraryType, 422 TYPE_REQUIRED. To attach the upload to a title you already have, send
assetId instead of those three. If you pass bytes and it is over 4.5 GiB, the call fails with 413 before any bytes move.
PUT the file
Send the bytes to uploadUrl with a single PUT. Use the same Content-Type you passed as contentType (video/mp4 if you
left it out). The URL is valid for one hour; it is signed, so don't add your API key to this request.
curl -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @the-long-night.mp4If it expires or the transfer dies, POST /v1/studio/jobs/{id}/upload-url returns a fresh { jobId, uploadUrl, key }. That call
signs for video/mp4, so upload with Content-Type: video/mp4 after re-issuing. It is refused with 409 once the bytes have landed.
Complete the upload
curl -X POST https://dev.app.ingenee.ai/api/v1/studio/jobs/a41c9e07-3b52-4d68-b1f0-7c2e5d8a9b13/complete-upload \
-H "Authorization: Bearer $INGENEE_API_KEY"{ "jobId": "a41c9e07-3b52-4d68-b1f0-7c2e5d8a9b13", "bytes": 2147483648, "library": { "createdAssetId": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11" } }Ingenee checks the file arrived (422 if not), re-checks its real size, hashes it, then creates the Library title and starts
processing. library is one of:
{ "createdAssetId": "…" }: a new title was created in your catalog.{ "duplicateOf": { "assetId": "…", "title": "…" } }: these exact bytes were uploaded before, so the item attached to that title and no new title was made.{ "error": "…" }: the file is stored but the title step failed; the message says why.
library is absent when the upload was attached to a title from the start.
Follow the job
Register a media-job-finished webhook rather than polling. Otherwise poll GET /v1/studio/jobs/{id}, which
returns { job, steps, objects }: job.status is pending, running, insufficient_credits, completed,
completed_with_warnings, failed or canceled; steps show each stage with the credits it settled; objects are the files
produced (renditions, transcript, thumbnails and so on).
Import from a URL or S3
curl -X POST https://dev.app.ingenee.ai/api/v1/studio/jobs \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "source": "url", "sourceRef": "https://media.example.com/the-long-night.mp4", "title": "The Long Night", "operations": ["thumbnails"] }'source is url or s3 (sourceRef is then an s3://bucket/key URI). Ingenee fetches the file itself and starts at once:
201 with { jobId, mediaItemId }. An import creates a media item, not a Library title; attach it to a title when you're ready
(see Attach to a title), or pass assetId to attach from the start. Importing a source that was already
imported returns 409 CONFLICT with the earlier details.jobId.
For many files, POST /v1/studio/bulk takes { "items": [{ "title", "source": "url", "sourceRef" }], "operations": […] } (up to 500 items) or an
"mrssUrl" to expand a feed, and returns { batchId, created, skipped, jobs }.
Operations
operations on a job, and the operation names in an estimate, come from this list. Processing steps (intake, probe, preview
rendition) always run; the rest run only if you ask.
| Operation | What it does |
|---|---|
local_analysis | Scene and shot analysis |
transcribe_assemblyai, transcribe_elevenlabs | AI transcription (the second is the fallback) |
video_understand_twelvelabs | AI video understanding |
identify_people | AI face analysis |
credits_extract | AI scan of the end credits |
scene_metadata | AI scene labels |
thumbnails | Best-frame stills |
ad_context | AI ad context |
editorial_pass | Editorial summary built on ad context |
dub_per_language, subtitle_translate | Billed per language by the dub and translate calls below |
If an operation can't run, the job creation returns 422 ("Not yet available") and nothing is charged.
Run operations on an item that's already processed
curl -X POST https://dev.app.ingenee.ai/api/v1/studio/items/e0b7d3a2-6f14-4c85-9a2d-1b3c5e7f9a04/operations \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "operations": ["transcribe_assemblyai", "ad_context"] }'{ "jobId": "c7d2e9f1-0a3b-4c5d-8e6f-1a2b3c4d5e6f", "operations": ["transcribe_assemblyai", "ad_context"] }You can send 1 to 11 operations from local_analysis, transcribe_assemblyai, transcribe_elevenlabs,
video_understand_twelvelabs, identify_people, credits_extract, scene_metadata, thumbnails, ad_context and
editorial_pass. Each reuses what processing already produced, so it can refuse with 422 and a message such as
local_analysis needs the 360p rendition, or ad_context needs a transcript or analysis. Run the prerequisite in the same call
or earlier. Only one job runs per item at a time: otherwise 409 CONFLICT with details.jobId.
Resume, reprocess, and handle credit shortfalls
| Call | Use |
|---|---|
POST /v1/studio/jobs/{id}/resume | Retry a job in insufficient_credits or failed, for example after credits are added. Other statuses return 422 |
POST /v1/studio/jobs/{id}/reprocess | Rebuild the normalized outputs from the stored raw results, without calling the AI services again |
A call that needs more credits than you have returns 409 INSUFFICIENT_CREDITS with the amounts in the message.
Read jobs and items
| Call | Returns |
|---|---|
GET /v1/studio/jobs | { jobs, total, page, pageSize }. Filters: assetId, status, source, q; page, pageSize, sort |
GET /v1/studio/items | { items, total, page, pageSize, summary }. Filters: assetId, status, q; page, pageSize, sort |
GET /v1/studio/items/{id} | { item, job, objects, adContext, analysis, transcript, scenes, sheet, otherColumns, dubSteps } |
DELETE /v1/studio/items/{id} | Deletes the item, its jobs and its stored files. 409 while a job is running |
Attach to a title
curl -X POST https://dev.app.ingenee.ai/api/v1/studio/items/e0b7d3a2-6f14-4c85-9a2d-1b3c5e7f9a04/attach \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "assetId": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11" }'{ "attached": true, "assetId": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11", "subtitlesMaterialized": true, "adContextLinked": false }Attaching never moves a title out of its catalog. If the title has no catalog, you must also send catalogId or
newCatalogName, or the call returns 422 CATALOG_REQUIRED. If the item has a transcript, it is added to the title's
subtitle tracks as a pending track for a person to review (subtitlesMaterialized: true).
Thumbnails and metadata
POST /v1/studio/items/{id}/thumbnails/framegrabs a still:{ "orientation": "landscape", "t": 754.5 }(portraitalso allowed;tin seconds, within the video's length).POST /v1/studio/items/{id}/thumbnails/choosepicks the thumbnail for an orientation:{ "orientation": "landscape", "objectId": "…" };objectId: nullreturns to the default.PATCH /v1/studio/items/{id}/metadataedits one field,{ "field": "synopsis", "value": "…" }(nullreverts to the layer underneath, a list field takes an array), or re-runs the AI with your values as known truth,{ "rerun": true }. A field measured from the file can't be changed inline (422).
Subtitles
Subtitle tracks belong to a title. AI-made tracks are proposals until approved.
curl -X POST https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/subtitles/generate \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" -d '{}'202 with { id, status: "transcribing", mediaKind }. It uses the title's media, or send mediaUrl; clipSeconds limits the
length. Only one transcription runs at a time (409 CONFLICT: retry later), and a title with no media returns 400 NO_MEDIA.
GET /v1/titles/{id}/subtitles lists tracks with status and locked.
| Call | Body | Notes |
|---|---|---|
POST /v1/subtitles/{sid}/decide | { "action": "approve" } | approve, reject, edit (with segments), lock, unlock. A locked track can't change except by reject (409) |
POST /v1/subtitles/{sid}/translate | { "locales": ["es", "fr"] } | The source track must be approved (409). Returns { created, skipped } |
GET /v1/subtitles/{sid}/export?format=vtt | none | srt (default) or vtt, as text |
Translate subtitles and dub (billed per language)
Both take a confirmedCredits figure that must equal what Ingenee is about to hold. Get it from the estimate: the
media_intelligence total for subtitle_translate or dub_per_language at the job's probedDurationSec, times the number of
languages. If the rate changed, the call returns 409 COST_CHANGED with the new total; confirm again.
curl -X POST https://dev.app.ingenee.ai/api/v1/studio/jobs/a41c9e07-3b52-4d68-b1f0-7c2e5d8a9b13/dub \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "languages": ["es", "fr"], "confirmedCredits": 36 }'{ "jobId": "a41c9e07-3b52-4d68-b1f0-7c2e5d8a9b13", "languages": ["es", "fr"], "heldCredits": 36, "perLanguage": 18 }Dubbing takes up to 10 language codes and needs a measured duration (422 before processing has run). For subtitle translation
on a media item use POST /v1/studio/items/{id}/translate-subs with { "languages": [{ "code": "es", "name": "Spanish" }], "confirmedCredits": 4 } (up to 8
languages); it needs a transcript first (422). Both return 202, and 409 while another job runs on the item.
Not yet available
- Uploads larger than 4.5 GiB (multipart upload).
- Deciding a possible-duplicate warning on a media item (attach to the existing title or keep both).
- Creating a Library title from an existing media item in one call. Attach it to a title instead.
Endpoints
| Operation | Method + path | Scope |
|---|---|---|
| Estimate credits | GET /v1/studio/estimate | studio:read |
| Credit balance | GET /v1/studio/credits | studio:read |
| Credit ledger | GET /v1/studio/credits/ledger | studio:read |
| Start an upload or URL import | POST /v1/studio/jobs | studio:write |
| List jobs | GET /v1/studio/jobs | studio:read |
| Get a job | GET /v1/studio/jobs/{id} | studio:read |
| Fresh upload URL | POST /v1/studio/jobs/{id}/upload-url | studio:write |
| Finish an upload | POST /v1/studio/jobs/{id}/complete-upload | studio:write |
| Resume a job | POST /v1/studio/jobs/{id}/resume | studio:write |
| Re-run processing | POST /v1/studio/jobs/{id}/reprocess | studio:write |
| Dub into another language | POST /v1/studio/jobs/{id}/dub | studio:write |
| List media items | GET /v1/studio/items | studio:read |
| Get a media item | GET /v1/studio/items/{id} | studio:read |
| Delete a media item | DELETE /v1/studio/items/{id} | studio:write |
| Attach to a title | POST /v1/studio/items/{id}/attach | studio:write |
| Run operations | POST /v1/studio/items/{id}/operations | studio:write |
| Edit metadata | PATCH /v1/studio/items/{id}/metadata | studio:write |
| Translate subtitles | POST /v1/studio/items/{id}/translate-subs | studio:write |
| Thumbnail from a frame | POST /v1/studio/items/{id}/thumbnails/frame | studio:write |
| Choose a thumbnail | POST /v1/studio/items/{id}/thumbnails/choose | studio:write |
| Bulk Studio actions | POST /v1/studio/bulk | studio:write |
| Generate subtitles | POST /v1/titles/{id}/subtitles/generate | studio:write |
| List subtitle tracks | GET /v1/titles/{id}/subtitles | studio:read |
| Approve or reject a track | POST /v1/subtitles/{sid}/decide | studio:write |
| Translate a track | POST /v1/subtitles/{sid}/translate | studio:write |
| Download a track | GET /v1/subtitles/{sid}/export | studio:read |