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/estimate and check your balance with GET /v1/studio/credits. A job that can't hold enough credits stops with status insufficient_credits and 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 returns 413 TOO_LARGE.
  • Every title lives in a catalog you choose. An upload that will become a new title needs catalogId or newCatalogName, and a libraryType.
  • 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"
200 OK (trimmed)
{
  "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"]
  }'
201 Created
{
  "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.mp4

If 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"
200 OK
{ "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.

OperationWhat it does
local_analysisScene and shot analysis
transcribe_assemblyai, transcribe_elevenlabsAI transcription (the second is the fallback)
video_understand_twelvelabsAI video understanding
identify_peopleAI face analysis
credits_extractAI scan of the end credits
scene_metadataAI scene labels
thumbnailsBest-frame stills
ad_contextAI ad context
editorial_passEditorial summary built on ad context
dub_per_language, subtitle_translateBilled 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"] }'
201 Created
{ "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

CallUse
POST /v1/studio/jobs/{id}/resumeRetry a job in insufficient_credits or failed, for example after credits are added. Other statuses return 422
POST /v1/studio/jobs/{id}/reprocessRebuild 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

CallReturns
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" }'
200 OK
{ "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/frame grabs a still: { "orientation": "landscape", "t": 754.5 } (portrait also allowed; t in seconds, within the video's length).
  • POST /v1/studio/items/{id}/thumbnails/choose picks the thumbnail for an orientation: { "orientation": "landscape", "objectId": "…" }; objectId: null returns to the default.
  • PATCH /v1/studio/items/{id}/metadata edits one field, { "field": "synopsis", "value": "…" } (null reverts 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.

CallBodyNotes
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=vttnonesrt (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 }'
202 Accepted
{ "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

OperationMethod + pathScope
Estimate creditsGET /v1/studio/estimatestudio:read
Credit balanceGET /v1/studio/creditsstudio:read
Credit ledgerGET /v1/studio/credits/ledgerstudio:read
Start an upload or URL importPOST /v1/studio/jobsstudio:write
List jobsGET /v1/studio/jobsstudio:read
Get a jobGET /v1/studio/jobs/{id}studio:read
Fresh upload URLPOST /v1/studio/jobs/{id}/upload-urlstudio:write
Finish an uploadPOST /v1/studio/jobs/{id}/complete-uploadstudio:write
Resume a jobPOST /v1/studio/jobs/{id}/resumestudio:write
Re-run processingPOST /v1/studio/jobs/{id}/reprocessstudio:write
Dub into another languagePOST /v1/studio/jobs/{id}/dubstudio:write
List media itemsGET /v1/studio/itemsstudio:read
Get a media itemGET /v1/studio/items/{id}studio:read
Delete a media itemDELETE /v1/studio/items/{id}studio:write
Attach to a titlePOST /v1/studio/items/{id}/attachstudio:write
Run operationsPOST /v1/studio/items/{id}/operationsstudio:write
Edit metadataPATCH /v1/studio/items/{id}/metadatastudio:write
Translate subtitlesPOST /v1/studio/items/{id}/translate-subsstudio:write
Thumbnail from a framePOST /v1/studio/items/{id}/thumbnails/framestudio:write
Choose a thumbnailPOST /v1/studio/items/{id}/thumbnails/choosestudio:write
Bulk Studio actionsPOST /v1/studio/bulkstudio:write
Generate subtitlesPOST /v1/titles/{id}/subtitles/generatestudio:write
List subtitle tracksGET /v1/titles/{id}/subtitlesstudio:read
Approve or reject a trackPOST /v1/subtitles/{sid}/decidestudio:write
Translate a trackPOST /v1/subtitles/{sid}/translatestudio:write
Download a trackGET /v1/subtitles/{sid}/exportstudio:read

On this page