# Video translation API

Canonical docs: https://platform.sonilo.com/docs/api/video-translation
Source path: /docs/api/video-translation
Runtime API host: https://api.sonilo.com/v1

Do not send API requests to platform.sonilo.com, docs.sonilo.com, or local docs URLs. Documentation pages are for reading only. Send runtime requests to https://api.sonilo.com/v1 with Authorization: Bearer sk_your_api_key.

## Endpoint

POST /v1/dubbing

## Summary

Translate one video into several languages in a single async call, returning one dubbed .mp4 per requested language. Translates and re-voices the existing speech; it does not generate music or sound effects.

## Use This For

- Localizing a finished video into several languages without re-recording or re-editing it.
- Producing a per-language deliverable set from one source, in one call and one charge rather than one call per language.

## Request

- video: file upload. Provide exactly one of video or video_url.
- video_url: URL input, https only — plain http is rejected, not merely discouraged. Provide exactly one of video or video_url. Max file size 400MB: when the server reports a Content-Length, a larger file is rejected with 413 before anything is charged.
- languages: optional JSON array string of language codes, e.g. ["pt_br", "th"]. Defaults to ["zh_cn", "es", "fr"] when omitted — which bills for three languages, so pass an explicit single-element array if only one is wanted. Supported: en, zh_cn, ja, ko, pt, pt_br, es, es_419, de, fr, it, ru, th, ar, tr, vi, id, ta, ml, kn, gu, pa_in, sd_in, hi. pt_br is Brazilian Portuguese and es_419 Latin American Spanish; plain pt and es are unqualified Portuguese and Spanish, so they are a third option rather than synonyms. ar is unqualified Arabic rather than one of the country dialects. pa_in and sd_in are the Indian variants of Punjabi and Sindhi, the only ones available. An unsupported code is rejected with 422 before anything is charged.
- ducking: optional boolean, default false. Ducks the background music/effects bed under the dubbed voice while it speaks; the bed is kept either way, and this is free.
- lipsync: optional boolean, default true. With lip sync on, the speaker's mouth is re-rendered to match the dubbed speech. Pass lipsync=false to leave the picture completely untouched instead — the video comes back at its original resolution and frame rate rather than re-rendered, and only the audio is replaced, so the mouths keep moving to the original language. Use it for footage with no on-camera speaker, or when preserving the exact original picture matters more than matching lip movement. The background bed is rebuilt either way, so ducking is unaffected.
- subtitles[<language>]: optional multipart field, one per language in languages, e.g. subtitles[ja]=@episode1.ja.srt or subtitles[tr]=https://cdn.example.com/episode1.tr.vtt. Each entry is either an uploaded .srt/.vtt file (max 1 MiB) or a public https URL. Target-language scripts the dub speaks verbatim instead of translating the source speech.
- export_srt: optional boolean, default false. Requires subtitles[<language>]. The finished task then also carries subtitles, a map of language code to the same lines re-timed against the dubbed audio as an .srt URL.
- stems: optional boolean, default false. The finished task also carries stems: per language, dubbed_dialogue, original_dialogue and background, each a WAV (48 kHz stereo) download URL, plus music and effects: the background split into music and sound effects, at the background's level; when present they come as a pair, and a task can come without them. Not covered by the free preview.
- voice: optional voice ID from GET /v1/dubbing/voices. Every line is spoken in that voice instead of a voice modeled on the original speaker. Requires subtitles[<language>] for every language. A line longer than its cue runs into the pause before the next cue, then is sped up by at most 1.3x. With voice or voices, each script may have at most 2 cues per second of video, 500 characters per cue and 30 characters per second of video.
- voices: optional JSON object of speaker label to voice ID, e.g. {"speaker_0": "sv_..."}, max 32 speakers. Requires speakers and subtitles[<language>] for every language. Send voice, or voices with speakers, not both. Use the suggested_voices of a proofread result requested with detect_speakers=true, or change it.
- speakers: optional JSON array with one speaker label per script cue, in cue order — the speakers array of a proofread result requested with detect_speakers=true. Every label needs a voice in voices, and every script must have exactly as many cues.
- emotions: optional JSON array with one emotion per script cue, in cue order, e.g. ["neutral", "excited", "sad"] — the emotions array of a proofread result requested with detect_emotions=true. One of neutral, happy, excited, sad, angry, scared, surprised, whisper, shout. Only with voice, or voices with speakers; every script must have exactly as many cues as emotions has entries. Voices whose age is child speak every line neutrally.

## Response And Errors

- 202 returns task_id and status=processing, plus subtitle_preflight, keyed by language, when subtitle scripts were supplied, and trial_preview when the run was the account's free preview.
- Poll GET /v1/tasks/{task_id} every 2-3 seconds.
- On success, read outputs — a map of language code to dubbed .mp4 URL. There is no single output_url, and no audio.url: a three-language request returns three entries keyed exactly by the codes that were requested.
- With subtitle scripts, the succeeded task also carries subtitle_preflight (per language: status, cue_count, issues, changes_count, report_url) and, with export_srt, subtitles (language code to re-timed .srt URL) and subtitle_export (per language: status — exported, exported_review_required or blocked — issues, alignment_loss, error, report_url). A blocked export never fails the task.
- Every subtitle script is validated before anything is billed, including that its text matches the declared language: a rejected script returns 422 with code SUBTITLE_PREFLIGHT_BLOCKED and the reports keyed by language under details. The set of languages in subtitles[<language>] must equal languages exactly (422 naming the missing/extra codes), the same subtitles[<language>] sent twice is 422, a bare subtitles field is 422, an empty subtitles[<language>] value is 422, and export_srt=true without any subtitles[<language>] is also 422.
- Billed as video duration multiplied by the number of languages requested, with a 10-second floor applied after that multiplication rather than per language. Free trial: 1 run per account, as a 15-second preview — the first 15 seconds of the video, in one language, with no subtitles — billed at $0. The 202 and the task then carry trial_preview: preview_seconds, source_duration_seconds, trimmed, languages, full_video_cost_usd (what translating the full video costs) and message. A request with several languages or a script is billed as usual; the free preview is not applied to it.
- With stems=true the succeeded task also carries stems: a map of language code to dubbed_dialogue, original_dialogue and background WAV URLs, plus music and effects (the background split into music and sound effects, 32-bit float at the background's level, adding up closely, not exactly, to background) when present; they come as a pair, and a task can come without them. A language whose stems could not be produced is missing; its video is still in outputs.
- An unknown voice ID, a voice that does not speak a requested language, a speaker with no voice, a speakers count that does not match the scripts, an unknown emotion, emotions without voice or voices, or an emotions count that does not match the scripts is 422 before anything is billed.
- Source videos may be at most 300 seconds.
- 401 auth_invalid, 402 payment_required or insufficient_balance, 403 forbidden, 404 not_found, 422 unprocessable_entity (unsupported language code, over-limit duration, or a non-https video_url), and 429 Retry-After should be handled explicitly.

## Example

```bash
curl -X POST "https://api.sonilo.com/v1/dubbing" \
  -H "Authorization: Bearer sk_your_api_key" \
  -F "video_url=https://example.com/clip.mp4" \
  -F 'languages=["pt_br", "es_419", "th"]'
```

## Shared Error Handling

- 401 auth_invalid: set Authorization: Bearer sk_your_api_key on the request.
- 402 payment_required or insufficient_balance: add credits before rerunning; do not create fake audio.
- 403 forbidden: the key is valid but the account or service permission is missing.
- 404 not_found: check the endpoint path and host. Use https://api.sonilo.com/v1, not a docs URL.
- 429 rate_limit_exceeded: honor Retry-After and retry with backoff.

## Endpoint Vocabulary

- GET /v1/account/services
- GET /v1/account/usage
- POST /v1/text-to-music
- POST /v1/video-to-music
- POST /v1/audio-ducking
- POST /v1/text-to-sfx
- POST /v1/video-to-sfx
- POST /v1/video-to-video-sfx
- POST /v1/video-to-sound
- POST /v1/video-to-video-sound
- GET /v1/tasks/{task_id}

