--- 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
#### Request Body
{
  "filename": "string",
  "content_type": "string"
}
**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
}
**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
#### Request Body
{
  "source_url": "string",
  "target_lang": "string",
  "source_lang": "string",
  "reduce_accent": "boolean",
  "translation_lag_time": "number",
  "original_speech_volume": "number"
}
**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)
{
  "job_id": "string",
  "status": "string",
  "source_lang": "string",
  "target_lang": "string",
  "created_at": "string",
  "limits": {
    "max_duration_sec": 0,
    "max_file_size_bytes": 0
  }
}
**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}
#### 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"
}
**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
#### 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
}
**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
#### Response
{
  "job_id": "string",
  "download_url": "string",
  "expires_at": "string"
}
**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"
  }
}
### 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