Job Lifecycle

View as Markdown

Dubbing runs asynchronously: you start a job, then follow it until the files are ready to download. This page covers every state a job passes through and the two status endpoints you read along the way.

If you just want the five calls in order, start with the Dubbing Overview.

Job statuses

StatusMeaningTerminal
not_startedCreated, but start has not been called. This is the status immediately after create().No
queuedstart accepted; waiting for a worker.No
in_progressThe job is being processed.No
completedEvery target language finished processing.Yes
partial_failureSome target languages succeeded, others failed. Real outputs exist for the ones that worked.Yes
failedThe job could not be completed. Read error_message.Yes
deletedThe job was deleted.Yes

partial_failure is uncommon, but treat it as terminal alongside completed. It carries real output for the languages that succeeded, so code that waits only for completed will poll until its own timeout and leave those files uncollected.

How a job moves

1

create() → not_started

The job row exists and you hold a job_id plus a signed upload_url. Nothing is processing yet.

2

start() → queued

Starting is idempotent in practice: calling it on a job that is already queued, in_progress, or completed returns without re-queueing.

3

Processing → in_progress

The job flips to in_progress once processing begins, and stays there for the bulk of the run.

4

Finished → completed, partial_failure, or failed

Exports are then triggered automatically for the formats in export_options.

completed means the dub itself finished. The exported files may still be rendering, and progress can sit just short of 100 while they do, so confirm in export-status before downloading.

Reading live-status

Alongside status, this endpoint reports a progress percentage and a human-readable current_step_label you can surface directly in a UI.

1{
2 "data": {
3 "job_id": "3f9a...",
4 "job_name": "keynote.mp4",
5 "status": "in_progress",
6 "current_step": "translate",
7 "current_step_label": "Translating",
8 "progress": 50,
9 "export": null,
10 "exports": null,
11 "error_message": null
12 }
13}

export and exports swap depending on language count. A job with one target language populates export (an object) and leaves exports null; a job with two or more does the reverse. An integration written against a single-language job therefore reads null the first time someone asks for two, so handle both keys.

Both are null until the job reaches completed. Note also that this block only ever carries a dubbed_video_url — there are no audio or SRT links here, which is why export-status is the endpoint to use for downloads.

Reading export-status

export-status is the authoritative source for downloads. It returns a flat array with one entry per (language, format), so a two-language, three-format job returns six entries in a single poll.

1{
2 "data": {
3 "exports": [
4 { "id": "a1...", "target_language": "hi-IN", "export_type": "video", "status": "completed", "is_stale": false, "created_at": "...", "completed_at": "...", "download_url": "https://..." },
5 { "id": "b2...", "target_language": "hi-IN", "export_type": "srt", "status": "completed", "is_stale": false, "created_at": "...", "completed_at": "...", "download_url": "https://..." },
6 { "id": "c3...", "target_language": "ta-IN", "export_type": "video", "status": "in_progress", "is_stale": false, "created_at": "...", "completed_at": null }
7 ]
8 }
9}

Group by target_language on your side, and take only entries whose own status is completed.

download_url is absent rather than null until an export completes, as in the third entry above. If you are reading the raw JSON, access it defensively; the Python SDK exposes it as an optional field, so it reads as None.

Set limit deliberately. It defaults to 5, so a job with 2 languages × 3 formats returns only the first five of its six entries, with no indication that more exist. Pass a value comfortably above languages × formats (maximum 100).

Each entry also carries is_stale, which becomes true when translation chunks for that language were edited after the export finished — the file you would download no longer matches the latest text. Re-export before publishing. Download URLs are signed and expire in roughly 24 hours, so persist the job_id and re-poll for a fresh link rather than caching the URL.

A complete run

Here is one job carried from an empty script to downloaded files, a block at a time. Paste the blocks into a single file in order and you have a working program; each one explains what it does and why it is needed.

1

Install the SDK and set your key

$pip install sarvamai
$export SARVAM_API_KEY="your-key-here"

Reading the key from an environment variable keeps it out of your source code, so you can share the script without sharing your credentials. Generate a key at Key Management.

2

Set up the client and your inputs

1import os, time
2from pathlib import Path
3from sarvamai import SarvamAI
4
5client = SarvamAI(api_subscription_key=os.environ["SARVAM_API_KEY"])
6
7media = Path("sample.mp4")
8target_language_codes = ["hi-IN", "ta-IN"]
9export_options = ["video", "srt"]

client is the object every call goes through. The three values below it are the only things you would normally change: the file to dub, the languages to dub it into, and the formats you want back. Naming them once here matters, because later steps reuse them to work out exactly what to wait for.

3

Create the job

1created = client.dubbing.create(
2 source_language_code="en-IN",
3 target_language_codes=target_language_codes,
4 export_options=export_options,
5 voice_cloning=True,
6 num_speakers=1,
7 job_name=media.name,
8)
9job_id = created.data.job_id

This registers the job and reserves somewhere to put your file. Nothing is processing yet and the file has not been sent. What comes back is a job_id, which identifies this job in every later call, and an upload_url, which is a temporary link to upload the file to.

voice_cloning=True keeps each original speaker’s voice, and num_speakers=1 says the source has one person talking. Both are covered in Choose a Voice.

4

Upload the media, then start the job

1client.dubbing.upload(created.data.upload_url, media)
2client.dubbing.start(job_id=job_id)

Uploading sends the file straight to storage rather than through the API, which is why it is a separate call from create. Starting then tells the service the file is in place and work can begin.

Order matters: starting before the file is uploaded leaves the job sitting at queued with nothing to process.

5

Wait for the dub to finish

1TERMINAL = {"completed", "partial_failure", "failed", "deleted"}
2
3deadline = time.monotonic() + 3600
4while True:
5 job = client.dubbing.get_live_status(job_id=job_id).data
6 print(f"{job.progress:>3}% {job.current_step_label}")
7
8 if job.status in TERMINAL:
9 break
10 if time.monotonic() > deadline:
11 raise TimeoutError(f"{job_id} did not finish in time")
12
13 time.sleep(15)

Dubbing takes minutes rather than seconds, so instead of waiting on one long request you ask for the status every 15 seconds until the job stops changing.

The loop exits on any status in TERMINAL, not just completed. That matters for partial_failure, which means some languages finished and others did not — there are still real files to collect, so it counts as done. The deadline is a safety net so the loop cannot run forever.

6

Stop if the job failed outright

1if job.status == "failed":
2 raise RuntimeError(job.error_message or "job failed")

failed is the only terminal status with nothing to collect. Everything else, partial_failure included, has output worth fetching, so the script continues.

7

Wait for the exported files

1wanted = {(lang, fmt) for lang in target_language_codes for fmt in export_options}
2
3deadline = time.monotonic() + 3600
4while True:
5 exports = client.dubbing.get_export_status(job_id=job_id, limit=100).data.exports or []
6 settled = {
7 (e.target_language, e.export_type)
8 for e in exports
9 if e.status in {"completed", "failed"}
10 }
11
12 if wanted <= settled:
13 break
14 if time.monotonic() > deadline:
15 raise TimeoutError(f"{job_id} exports did not settle in time")
16
17 time.sleep(15)

A finished job does not yet mean finished files, because each output is rendered after the dub itself completes. This is a second wait, for the files rather than the dub.

wanted is every combination you asked for: two languages times two formats is four files. The loop keeps polling until each of those has settled, meaning it either completed or failed, so one slow file cannot make you miss the others. limit=100 is deliberate — the default of 5 would not return every entry.

client.dubbing.upload() requires sarvamai 0.1.31a1 or newer.

Poll from a background worker rather than an HTTP request handler, and always cap total wait time as above. The API does not mandate an interval; the 15 seconds here is a starting point to tune against your own file lengths, not a recommended setting.

You do not need to add retry logic for server errors. The SDK already retries 429, 408, 409, and 5xx responses with exponential backoff, honouring Retry-After, twice by default. Raise it per call with request_options={"max_retries": 5}. A dropped connection is not retried, so wrap the calls in your own try / except httpx.TransportError if the script runs unattended.

1import os, time
2from pathlib import Path
3from sarvamai import SarvamAI
4
5client = SarvamAI(api_subscription_key=os.environ["SARVAM_API_KEY"])
6
7media = Path("sample.mp4")
8target_language_codes = ["hi-IN", "ta-IN"]
9export_options = ["video", "srt"]
10
11created = client.dubbing.create(
12 source_language_code="en-IN",
13 target_language_codes=target_language_codes,
14 export_options=export_options,
15 voice_cloning=True,
16 num_speakers=1,
17 job_name=media.name,
18)
19job_id = created.data.job_id
20
21client.dubbing.upload(created.data.upload_url, media)
22client.dubbing.start(job_id=job_id)
23
24TERMINAL = {"completed", "partial_failure", "failed", "deleted"}
25
26deadline = time.monotonic() + 3600
27while True:
28 job = client.dubbing.get_live_status(job_id=job_id).data
29 print(f"{job.progress:>3}% {job.current_step_label}")
30
31 if job.status in TERMINAL:
32 break
33 if time.monotonic() > deadline:
34 raise TimeoutError(f"{job_id} did not finish in time")
35
36 time.sleep(15)
37
38if job.status == "failed":
39 raise RuntimeError(job.error_message or "job failed")
40
41# partial_failure still has usable outputs, so keep going either way.
42wanted = {(lang, fmt) for lang in target_language_codes for fmt in export_options}
43
44deadline = time.monotonic() + 3600
45while True:
46 exports = client.dubbing.get_export_status(job_id=job_id, limit=100).data.exports or []
47 settled = {
48 (e.target_language, e.export_type)
49 for e in exports
50 if e.status in {"completed", "failed"}
51 }
52
53 if wanted <= settled:
54 break
55 if time.monotonic() > deadline:
56 raise TimeoutError(f"{job_id} exports did not settle in time")
57
58 time.sleep(15)
59
60downloads = {
61 (e.target_language, e.export_type): e.download_url
62 for e in exports
63 if e.status == "completed" and not e.is_stale
64}
65
66for (lang, fmt), url in sorted(downloads.items()):
67 print(f"{lang} {fmt}: {url}")

Troubleshooting

Almost everything below is a configuration detail rather than a dub that went wrong: a call that was never made, a URL that has aged out, or a parameter the API rejected up front.

SymptomCauseFix
Stuck in not_startedstart was never called.Call start after uploading.
Stuck in queuedThe media was never uploaded, so there is nothing to process.Upload to upload_url, then start.
403 on uploadThe signed URL expired, or the request is missing x-ms-blob-type: BlockBlob.Check expires_in_hours and upload promptly; create a new job for a fresh URL. Never send your API key to the storage URL.
422 on createAn invalid language code (Odia is or-IN, not od-IN), the source language repeated in target_langs, an unknown voice_id, or voice_cloning: false without a voice_id.See Language Codes and Voices.
export-status empty on a completed jobeditor_flow: true suppresses auto-export, leaving exports for a human to trigger in Creator Studio.Keep editor_flow at its default of false for API integrations.
Fewer exports than expectedlimit defaults to 5.Pass an explicit limit above languages × formats.

Log the job_id on every call. It is how a dub is traced end to end, and it is the first thing to include in a support request.