Job Lifecycle
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
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
create() → not_started
The job row exists and you hold a job_id plus a signed upload_url. Nothing is processing yet.
start() → queued
Starting is idempotent in practice: calling it on a job that is already queued, in_progress, or completed returns without re-queueing.
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.
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.
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.
Install the SDK and set your key
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.
Set up the client and your inputs
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.
Create the job
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.
Upload the media, then start the job
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.
Wait for the dub to finish
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.
Stop if the job failed outright
failed is the only terminal status with nothing to collect. Everything else, partial_failure included, has output worth fetching, so the script continues.
Wait for the exported files
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.
The whole script in one piece
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.
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.