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

ConstraintAccepted values
ContainersMP4, MOV, MKV, WebM, AVI, FLV, TS
Video codecsH.264, H.265/HEVC, VP8, VP9, AV1, ProRes, MPEG-4
Audio codecsAAC, MP3, Opus, Vorbis, PCM, FLAC, AC-3
AudioMust contain at least one audio track with speech

Audio files

ConstraintAccepted values
FormatsWAV, MP3, FLAC, OGG, M4A, AAC, WMA
AudioMust 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.

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:

FieldTypeRequiredDescription
filenamestringYesName of the file (e.g., "video.mp4", "audio.mp3")
content_typestringYesMIME 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:

FieldTypeDescription
upload_urlstringPresigned S3 PUT URL for uploading the file
source_urlstringThe URL to use as source_url when creating a dubbing job
upload_idstringUnique identifier for this upload
max_file_size_bytesnumberMaximum allowed file size in bytes (2 GB)
expires_in_secnumberSeconds 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:

FieldTypeRequiredDescription
source_urlstringYesURL of the audio or video file to dub (public URL or from upload endpoint)
target_langstringYesTarget language code (en, es, fr, de, it, pt, ru, ja, ko, zh)
source_langstringNoSource language code or "auto" for auto-detection (default: "auto")
reduce_accentbooleanNoReduce 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_timenumberNoSeconds to delay translated speech after original segment start. Creates a “live interpreter” effect. Range: 0–5. (default: 0 — off)
original_speech_volumenumberNoMix 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_fillersbooleanNoStrip filler words and disfluencies (“um”, “uh”, “you know”, false starts) from the spoken output. (default: false)
lipsyncbooleanNoRegenerate 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_srtstringNoAn 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
  }
}

Response Fields:

FieldTypeDescription
job_idstringUnique identifier for the dubbing job
statusstringCurrent job status (initially "pending")
source_langstringSource language (or "auto" if auto-detecting)
target_langstringTarget language code
created_atstringISO 8601 timestamp of job creation
limitsobjectJob limits applied
limits.max_duration_secnumberMaximum input duration in seconds
limits.max_file_size_bytesnumberMaximum 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.

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:

FieldTypeDescription
job_idstringUnique identifier for the dubbing job
statusstringCurrent status (see Status Flow below)
source_langstringSource language
target_langstringTarget language code
errorstring or nullError message if status is "failed"
progressobjectProgress details (stage and percent)
input_duration_secnumberDuration of the input media in seconds
cost_usdnumberCost charged for this job in USD
output_urlstring or nullPresigned download URL (available when "completed")
output_expires_atstring or nullISO 8601 expiry time of the download URL
subtitles_original_urlstring or nullPresigned .srt in the source language, timed to the dub
subtitles_translated_urlstring or nullPresigned .srt in the target language
lipsyncobject or nullPresent only when lipsync: true was requested. See below
created_atstringISO 8601 timestamp of job creation
updated_atstringISO 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.

FieldTypeDescription
requestedbooleanAlways true when the object is present
statusstringpending, ok, skipped, unsupported, or failed
errorstring or nullWhy 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

ParameterTypeRequiredDescription
limitnumberNoNumber of jobs to return (default: 20, max: 100)
offsetnumberNoOffset 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:

FieldTypeDescription
jobsarrayArray of job summary objects
totalnumberTotal number of jobs
limitnumberLimit used in the query
offsetnumberOffset 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:

FieldTypeDescription
job_idstringThe dubbing job identifier
download_urlstringFresh presigned download URL
expires_atstringISO 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

StatusDescription
pendingJob created, waiting to start
downloadingDownloading the source media
processingDubbing in progress (stages 1-7)
uploadingUploading the dubbed output
completedDone — output_url is available
failedAn 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” } }

Job-Level Errors

When a job fails, the error field in the job status response contains the error code:

Error CodeDescription
video_too_longInput exceeds 60 minute limit
video_too_shortInput is less than 1 second
video_too_largeFile size exceeds 2 GB limit
unsupported_formatMedia format or codec is not supported, or audio track is missing
download_failedCould not download the source media from the provided URL
lipsync_unavailablelipsync: 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_failedAn 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