---
title: "Dubbing API Reference"
section: "Dubbing"
order: 1
sidebarLabel: "API Reference"
---
## Overview
The Dubbing API is an asynchronous dubbing service. Submit an audio or video URL, and receive a fully dubbed file in your target language.
- **Pricing:** $0.50 per minute of input media, or $1.00 per minute with `lipsync: true`
- **Max duration:** 60 minutes
- **Max file size:** 2 GB
- **Min duration:** 1 second
- **Supported languages:** English (`en`), Spanish (`es`), French (`fr`), German (`de`), Italian (`it`), Portuguese (`pt`), Russian (`ru`), Japanese (`ja`), Korean (`ko`), Chinese (`zh`)
## Supported Formats
### Input requirements
#### Video files
| Constraint | Accepted values |
|---|---|
| **Containers** | MP4, MOV, MKV, WebM, AVI, FLV, TS |
| **Video codecs** | H.264, H.265/HEVC, VP8, VP9, AV1, ProRes, MPEG-4 |
| **Audio codecs** | AAC, MP3, Opus, Vorbis, PCM, FLAC, AC-3 |
| **Audio** | Must contain at least one audio track with speech |
#### Audio files
| Constraint | Accepted values |
|---|---|
| **Formats** | WAV, MP3, FLAC, OGG, M4A, AAC, WMA |
| **Audio** | Must contain speech |
The input must contain audible speech in a [supported language](#overview). Background music, sound effects, and multiple speakers are all handled automatically.
### Output format
The output format depends on the input type:
- **Video input** -- returned as **MP4** (H.264 video + AAC audio at 192 kbps). Resolution and frame rate match the input.
- **Audio-only input** -- returned as **MP3** (192 kbps) or **WAV** depending on the input format.
The output URL is a presigned S3 link valid for 7 days. The file itself is kept for 30 days, so a link that has expired can be refreshed from the job status endpoint.
### Providing a source URL
The `source_url` must be a publicly accessible HTTP(S) URL that returns the media file directly (not an HTML page). Good options:
- **Pinch upload endpoint** — Use the [Upload Media](#upload-media-optional) endpoint to get a presigned S3 URL. This is the simplest approach.
- **Your own S3 bucket** — Generate a presigned GET URL with a long enough expiry (we recommend at least 1 hour):
```python
import boto3
s3 = boto3.client("s3")
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "my-bucket", "Key": "media/input.mp4"},
ExpiresIn=3600,
)
```
- **Other cloud storage** — Any direct download URL works (Google Cloud Storage signed URLs, Azure Blob SAS URLs, Cloudflare R2 presigned URLs, etc.)
- **CDN / public URL** — Direct links to media files (e.g., `https://cdn.example.com/video.mp4`)
> **Note:** Private network URLs (localhost, internal IPs) are rejected for security. The URL must resolve to a public IP address.
## Authentication
All API requests require authentication using a Bearer token in the `Authorization` header.
Authorization header
Authorization: Bearer <your-api-token>
**Base URL:** `https://api.startpinch.com`
## Endpoints
### Upload Media (optional)
Returns a presigned S3 PUT URL for direct upload. Use this if you need to upload a local file before creating a dubbing job.
POST/api/dubbing/upload-url
Playground: enter your API key + fields below, then send a real request and inspect the JSON response.
**Response Fields:**
| Field | Type | Description |
| --- | --- | --- |
| `upload_url` | string | Presigned S3 PUT URL for uploading the file |
| `source_url` | string | The URL to use as `source_url` when creating a dubbing job |
| `upload_id` | string | Unique identifier for this upload |
| `max_file_size_bytes` | number | Maximum allowed file size in bytes (2 GB) |
| `expires_in_sec` | number | Seconds until the upload URL expires |
---
### Create Dubbing Job
Submit an audio or video file for dubbing. The file will be processed asynchronously.
POST/api/dubbing/jobs
Playground: enter your API key + fields below, then send a real request and inspect the JSON response.
**Parameters:**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `source_url` | string | Yes | URL of the audio or video file to dub (public URL or from upload endpoint) |
| `target_lang` | string | Yes | Target language code (`en`, `es`, `fr`, `de`, `it`, `pt`, `ru`, `ja`, `ko`, `zh`) |
| `source_lang` | string | No | Source language code or `"auto"` for auto-detection (default: `"auto"`) |
| `reduce_accent` | boolean | No | Reduce source language accent in dubbed audio. When set to True, produces more natural target language pronunciation at the cost of slightly less voice similarity. (default: `false`) |
| `translation_lag_time` | number | No | Seconds to delay translated speech after original segment start. Creates a "live interpreter" effect. Range: 0–5. (default: `0` — off) |
| `original_speech_volume` | number | No | Mix original speech back in at this volume, ducked automatically while translated speech plays. Range: 0–1. (default: `0` — the dub replaces the original speech entirely) |
| `remove_fillers` | boolean | No | Strip filler words and disfluencies ("um", "uh", "you know", false starts) from the spoken output. (default: `false`) |
| `lipsync` | boolean | No | Regenerate the speaker's mouth to match the dubbed audio. Video only. Billed at $1.00 per minute instead of $0.50. Returns a `lipsync_unavailable` error if the feature is not enabled — never a silently un-lipsynced dub. (default: `false`) |
| `transcript_srt` | string | No | An SRT file's contents supplying exactly what should be spoken and when. Translation is skipped and these cues are spoken verbatim in the cloned voice. Max 2 MB. See [Supplying your own script](#supplying-your-own-script) |
#### Regenerating in the same language
Setting `target_lang` to the language already being spoken re-speaks the file in
the speaker's own cloned voice instead of translating it. Paired with
`remove_fillers`, that is a cleanup pass over a rambling take.
It re-speaks the **entire file** — the original performance is replaced
everywhere, not only where the filler words were. That is the right tool for
tidying a whole recording, and the wrong one for correcting a few seconds of an
otherwise-good take.
#### Response (201 Created)
**Response Fields:**
| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | Unique identifier for the dubbing job |
| `status` | string | Current job status (initially `"pending"`) |
| `source_lang` | string | Source language (or `"auto"` if auto-detecting) |
| `target_lang` | string | Target language code |
| `created_at` | string | ISO 8601 timestamp of job creation |
| `limits` | object | Job limits applied |
| `limits.max_duration_sec` | number | Maximum input duration in seconds |
| `limits.max_file_size_bytes` | number | Maximum file size in bytes |
---
### Supplying your own script
Pass `transcript_srt` when the script already exists — a translation you have
reviewed, a transcript you have corrected, or a same-language rewrite — and it
is spoken verbatim instead of being translated. Everything else is unchanged:
voice cloning, timing, subtitles and lip sync all still apply.
The quickest way to get a starting file is to dub once, download
`subtitles_translated_url`, edit it, and resubmit against the same media.
```srt
1
00:00:00,000 --> 00:00:02,700
So, is medication alone really enough to lose weight?
2
00:00:02,800 --> 00:00:11,000
Whenever these drugs come up, people assume that simply taking them is enough.
```
Each cue is spoken inside its own window, and the two directions are handled
differently on purpose:
- **A line too long for its window** is sped up within an imperceptible margin.
If that is not enough it is rewritten shorter, to the same meaning, and
re-synthesised — the alternative is words clipped off the end.
- **A line shorter than its window** is left alone. The remaining time is a
pause. Nothing is invented to fill it, because you chose that window.
So supplying text longer than the original speech is safe, but the wording may
not survive verbatim. Check `subtitles_translated_url` on the finished job to
see what was actually spoken. Keeping each cue close to the length of the
speech it replaces avoids rewrites entirely.
Cues are parsed leniently — CRLF, a BOM, `.` instead of `,` before the
milliseconds, and missing index lines are all accepted. Cues with no valid
timestamp are skipped. Multi-line cues are joined with a space, since the line
breaks are a rendering concern rather than a pause.
---
### Get Job Status
Poll this endpoint to track the progress of a dubbing job.
GET/api/dubbing/jobs/{id}
Playground: enter your API key + job ID below, then send a real request and inspect the JSON response.
**Response Fields:**
| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | Unique identifier for the dubbing job |
| `status` | string | Current status (see Status Flow below) |
| `source_lang` | string | Source language |
| `target_lang` | string | Target language code |
| `error` | string or null | Error message if status is `"failed"` |
| `progress` | object | Progress details (stage and percent) |
| `input_duration_sec` | number | Duration of the input media in seconds |
| `cost_usd` | number | Cost charged for this job in USD |
| `output_url` | string or null | Presigned download URL (available when `"completed"`) |
| `output_expires_at` | string or null | ISO 8601 expiry time of the download URL |
| `subtitles_original_url` | string or null | Presigned `.srt` in the source language, timed to the dub |
| `subtitles_translated_url` | string or null | Presigned `.srt` in the target language |
| `lipsync` | object or null | Present only when `lipsync: true` was requested. See below |
| `created_at` | string | ISO 8601 timestamp of job creation |
| `updated_at` | string | ISO 8601 timestamp of last update |
#### The `lipsync` object
Lip sync is reported separately from `status`, because the two answer different
questions. A job whose lip sync failed still reaches `status: "completed"` and
still has a playable dub at `output_url` — the audio is identical either way,
lip sync only regenerates the mouth. Treating it as a job failure would hide a
delivered result.
| Field | Type | Description |
| --- | --- | --- |
| `requested` | boolean | Always `true` when the object is present |
| `status` | string | `pending`, `ok`, `skipped`, `unsupported`, or `failed` |
| `error` | string or null | Why it did not run, when `status` is `failed` or `unsupported` |
`unsupported` means the input turned out to have no video track. `failed` means
lip sync ran and errored; you still get the dub. If you need to know before
paying for a job, request `lipsync: true` at submit time and handle
`lipsync_unavailable` — that is rejected up front rather than downgraded.
Billing follows what was delivered, not what was asked for. A job billed at the
$1.00/min lip sync rate is one that came back `status: "ok"`. `skipped`,
`unsupported` and `failed` all hand you an ordinary dub, and are billed at
$0.50/min. Your balance is reserved at the lip sync rate while the job runs, so
a job that can't cover $1.00/min is refused before any GPU time is spent.
---
### List Jobs
Retrieve a paginated list of your dubbing jobs.
GET/api/dubbing/jobs?limit=20&offset=0
Playground: enter your API key below, then send a real request and inspect the JSON response.
**Response Fields:**
| Field | Type | Description |
| --- | --- | --- |
| `jobs` | array | Array of job summary objects |
| `total` | number | Total number of jobs |
| `limit` | number | Limit used in the query |
| `offset` | number | Offset used in the query |
---
### Get Download URL (refresh)
Generates a fresh presigned download URL for a completed dubbing job. Use this if the original `output_url` has expired.
GET/api/dubbing/jobs/{id}/result
Playground: enter your API key + job ID below, then send a real request and inspect the JSON response.
**Response Fields:**
| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | The dubbing job identifier |
| `download_url` | string | Fresh presigned download URL |
| `expires_at` | string | ISO 8601 expiry time of the download URL |
---
## Status Flow
Jobs progress through these statuses:
Status transitions
pending → downloading → processing → uploading → completed
On failure: any status → failed
| Status | Description |
| --- | --- |
| `pending` | Job created, waiting to start |
| `downloading` | Downloading the source media |
| `processing` | Dubbing in progress (stages 1-7) |
| `uploading` | Uploading the dubbed output |
| `completed` | Done -- `output_url` is available |
| `failed` | An error occurred -- check `error` field |
During `processing`, the `progress` object contains stage details:
```json
{
"stage": "stage_3",
"stage_name": "Translating",
"percent": 42
}
```
---
## Error Codes
// invalid_url
{
"error": {
"code": "invalid_url",
"message": "The provided source URL is not a valid URL"
}
}
// unsupported_language
{
"error": {
"code": "unsupported_language",
"message": "Unsupported target language: xx"
}
}
{
"error": {
"code": "unauthorized",
"message": "Invalid or expired API token"
}
}
{
"error": {
"code": "insufficient_balance",
"message": "Your account balance is too low to process this file"
}
}
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Maximum 10 requests per minute."
}
}
### Job-Level Errors
When a job fails, the `error` field in the job status response contains the error code:
| Error Code | Description |
| --- | --- |
| `video_too_long` | Input exceeds 60 minute limit |
| `video_too_short` | Input is less than 1 second |
| `video_too_large` | File size exceeds 2 GB limit |
| `unsupported_format` | Media format or codec is not supported, or audio track is missing |
| `download_failed` | Could not download the source media from the provided URL |
| `lipsync_unavailable` | `lipsync: true` was requested but the input has no video track, or lip sync is not enabled. Returned at submit time, so you never receive an un-lipsynced file believing otherwise |
| `processing_failed` | An internal error occurred during dubbing |
---
## Example: Full Workflow
A complete JavaScript example that uploads a file, creates a dubbing job, polls for completion, and downloads the result.
```jsx
const API_KEY = '';
const BASE_URL = 'https://api.startpinch.com';
const headers = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
};
// Step 1: Get a presigned upload URL
const uploadRes = await fetch(`${BASE_URL}/api/dubbing/upload-url`, {
method: 'POST',
headers,
body: JSON.stringify({
filename: 'my-video.mp4',
content_type: 'video/mp4'
})
});
const { upload_url, source_url } = await uploadRes.json();
// Step 2: Upload the file via PUT
const videoFile = /* your File or Blob */;
await fetch(upload_url, {
method: 'PUT',
headers: { 'Content-Type': 'video/mp4' },
body: videoFile
});
// Step 3: Create a dubbing job
const jobRes = await fetch(`${BASE_URL}/api/dubbing/jobs`, {
method: 'POST',
headers,
body: JSON.stringify({
source_url: source_url,
target_lang: 'es',
source_lang: 'auto'
})
});
const job = await jobRes.json();
console.log('Job created:', job.job_id);
// Step 4: Poll for job completion
let status = job.status;
while (status !== 'completed' && status !== 'failed') {
await new Promise(r => setTimeout(r, 5000)); // wait 5 seconds
const pollRes = await fetch(`${BASE_URL}/api/dubbing/jobs/${job.job_id}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const pollData = await pollRes.json();
status = pollData.status;
console.log(`Status: ${status}`, pollData.progress);
if (status === 'completed') {
// Step 5: Download the dubbed output
console.log('Download URL:', pollData.output_url);
const video = await fetch(pollData.output_url);
const blob = await video.blob();
// Save or use the blob as needed
}
if (status === 'failed') {
console.error('Job failed:', pollData.error);
}
}
```
---
## Guides
- [How to Dub Videos with Python](/guides/how-to-dub-videos-with-python) — complete Python tutorial with batch processing and error handling
- [AI Video Dubbing API](/guides/ai-video-dubbing-api) — how dubbing APIs work and what to look for
- [Dubbing API Pricing Comparison](/guides/dubbing-api-pricing-comparison) — Pinch vs ElevenLabs vs Rask.ai