Dubbing 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. 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 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):
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.
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.
/api/dubbing/upload-url
{
"upload_url": "https://...",
"source_url": "https://...",
"upload_id": "...",
"max_file_size_bytes": 500000000,
"expires_in_sec": 3600
}
Request Body
{
"filename": "string",
"content_type": "string"
}
{
"filename": "video.mp4",
"content_type": "video/mp4"
}
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
filename | string | Yes | Name of the file (e.g., "video.mp4", "audio.mp3") |
content_type | string | Yes | MIME type of the file (e.g., "video/mp4") |
Response
{
"upload_url": "string",
"source_url": "string",
"upload_id": "string",
"max_file_size_bytes": 0,
"expires_in_sec": 0
}
{
"upload_url": "https://s3.amazonaws.com/pinch-dubbing/uploads/abc123?X-Amz-Algorithm=...",
"source_url": "https://s3.amazonaws.com/pinch-dubbing/uploads/abc123",
"upload_id": "abc123",
"max_file_size_bytes": 500000000,
"expires_in_sec": 3600
}
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.
/api/dubbing/jobs
{
"job_id": "...",
"status": "pending",
"source_lang": "auto",
"target_lang": "es",
"created_at": "..."
}
Request Body
{
"source_url": "string",
"target_lang": "string",
"source_lang": "string",
"reduce_accent": "boolean",
"translation_lag_time": "number",
"original_speech_volume": "number"
}
{
"source_url": "https://s3.amazonaws.com/pinch-dubbing/uploads/abc123",
"target_lang": "es",
"source_lang": "auto",
"reduce_accent": false,
"translation_lag_time": 1.0,
"original_speech_volume": 0.25
}
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 |
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)
{
"job_id": "string",
"status": "string",
"source_lang": "string",
"target_lang": "string",
"created_at": "string",
"limits": {
"max_duration_sec": 0,
"max_file_size_bytes": 0
}
}
{
"job_id": "dub_7f3a1b2c",
"status": "pending",
"source_lang": "auto",
"target_lang": "es",
"created_at": "2026-03-05T14:30:00Z",
"limits": {
"max_duration_sec": 600,
"max_file_size_bytes": 500000000
}
}
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.
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.
/api/dubbing/jobs/{id}
{
"job_id": "...",
"status": "completed",
"output_url": "https://..."
}
Response
{
"job_id": "string",
"status": "string",
"source_lang": "string",
"target_lang": "string",
"error": "string | null",
"progress": {},
"input_duration_sec": 0,
"cost_usd": 0,
"output_url": "string | null",
"output_expires_at": "string | null",
"created_at": "string",
"updated_at": "string"
}
{
"job_id": "dub_7f3a1b2c",
"status": "completed",
"source_lang": "auto",
"target_lang": "es",
"error": null,
"progress": {
"stage": "completed",
"percent": 100
},
"input_duration_sec": 61.5,
"cost_usd": 0.51,
"output_url": "https://s3.amazonaws.com/pinch-dubbing/results/dub_7f3a1b2c.mp4?X-Amz-Algorithm=...",
"output_expires_at": "2026-03-06T14:30:00Z",
"created_at": "2026-03-05T14:30:00Z",
"updated_at": "2026-03-05T14:35:22Z"
}
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.
/api/dubbing/jobs?limit=20&offset=0
{
"jobs": [...],
"total": 5,
"limit": 20,
"offset": 0
}
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | Number of jobs to return (default: 20, max: 100) |
offset | number | No | Offset for pagination (default: 0) |
Response
{
"jobs": [
{
"job_id": "string",
"status": "string",
"source_lang": "string",
"target_lang": "string",
"created_at": "string",
"updated_at": "string"
}
],
"total": 0,
"limit": 0,
"offset": 0
}
{
"jobs": [
{
"job_id": "dub_7f3a1b2c",
"status": "completed",
"source_lang": "en",
"target_lang": "es",
"created_at": "2026-03-05T14:30:00Z",
"updated_at": "2026-03-05T14:35:22Z"
},
{
"job_id": "dub_9e2d4f8a",
"status": "processing",
"source_lang": "auto",
"target_lang": "fr",
"created_at": "2026-03-05T15:00:00Z",
"updated_at": "2026-03-05T15:02:10Z"
}
],
"total": 5,
"limit": 20,
"offset": 0
}
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.
/api/dubbing/jobs/{id}/result
{
"job_id": "...",
"download_url": "https://...",
"expires_at": "..."
}
Response
{
"job_id": "string",
"download_url": "string",
"expires_at": "string"
}
{
"job_id": "dub_7f3a1b2c",
"download_url": "https://s3.amazonaws.com/pinch-dubbing/results/dub_7f3a1b2c.mp4?X-Amz-Algorithm=...",
"expires_at": "2026-03-06T14:30:00Z"
}
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:
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:
{
"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.
const API_KEY = '<your-api-token>';
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 — complete Python tutorial with batch processing and error handling
- AI Video Dubbing API — how dubbing APIs work and what to look for
- Dubbing API Pricing Comparison — Pinch vs ElevenLabs vs Rask.ai