> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.voicecheap.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Dub Project

> Dub an already transcribed project from your own translated SRT

# Dub Project

Generate the voices and mix the output of an existing project from a translated SRT that you provide. VoiceCheap does
not translate anything on this endpoint: no translation engine, no SmartSync rephrasing, no glossary. Each cue is voiced
verbatim on the matching segment of the project transcript, then mixed with the original background, subtitles and
optional lip-sync exactly like a regular translation.

Use it when your translation is produced elsewhere (a human translator, your own tooling, an automation flow) and you
only need VoiceCheap for voice generation. The run is asynchronous: poll [Get Translation Status](/docs/api-reference/translation-status)
or listen to the [webhooks](/docs/api-reference/webhooks).

## Prerequisites

* The project must exist and its transcription must be complete: create it with [Create Project](/docs/api-reference/create-project)
  (transcription only) or [Start Translation](/docs/api-reference/translate), and wait for `transcriptionStatus: "success"`.
* Your SRT must cover the project transcript: every transcript segment needs at least one cue, and every cue must fall on
  a segment. Export the transcript with [Export Project Transcript](/docs/api-reference/project-transcript) and translate it cue
  by cue to keep the timecodes.
* The request is billed like a translation of the same media, with the same credit check. Lip-sync modes add their usual
  extra credits.

## Concurrency Limit

Project creation, translation, and dubbing all check your account’s active-translation limit (at most 10, possibly lower). Only in-progress translation or dubbing runs occupy slots; transcription-only projects do not. If the limit is reached, the API returns `CONCURRENT_TRANSLATION_LIMIT_REACHED` (HTTP 429). The applied limit is in `details.limit`.

A second run for the same project and target language is also rejected while one is in progress (`TRANSLATION_ALREADY_IN_PROGRESS`, HTTP 409).

## Request

This endpoint accepts a JSON body (`Content-Type: application/json`). Boolean and numeric options are sent as JSON values,
not as strings.

### Headers

<ParamField header="x-api-key" type="string" required>
  Your VoiceCheap API key. Get one from [app.voicecheap.ai/page-api](https://app.voicecheap.ai/page-api).
</ParamField>

### Path Parameters

<ParamField path="projectId" type="string" required>
  The project identifier returned by [Create Project](/docs/api-reference/create-project) or [Start Translation](/docs/api-reference/translate).
</ParamField>

### Body Parameters

<ParamField body="targetLanguage" type="string" required>
  The language of `translatedSrt`, lowercase, from the same list as [Start Translation](/docs/api-reference/translate). When the
  project has no work for this language yet, one is created and the language is added to the project.
</ParamField>

<ParamField body="translatedSrt" type="string" required>
  The final target-language SRT content (cue number, timing line, text). Maximum 1,000,000 characters. Cues are matched to
  the transcript segments by timestamps: a cue starting within 50 ms of a segment start is assigned to that segment,
  otherwise to the segment it overlaps the most. Several cues on one segment are joined with a space. The timing, speaker
  and order of every voiced segment always come from the project transcript; your timecodes are only used for matching.
</ParamField>

<ParamField body="webhookUrl" type="string">
  An https endpoint that receives the [webhook events](/docs/api-reference/webhooks) for this project from now on, overriding
  the endpoint configured on your account.

  **Default:** The endpoint already stored on the project, then the account webhook endpoint.
</ParamField>

<ParamField body="keepBackgroundMusic" type="boolean">
  Keep the original background audio (music, ambience) under the generated voices.

  **Default:** `true`
</ParamField>

<ParamField body="keepOriginalVoice" type="boolean">
  Keep the original speaker quietly underneath the generated voice.

  **Default:** `false`
</ParamField>

<ParamField body="originalVoiceVolume" type="number">
  Original-voice volume from `1` through `70` when `keepOriginalVoice` is enabled.

  **Default:** `30`
</ParamField>

<ParamField body="voiceMode" type="string">
  Voice strategy: `cloning` (clone the speakers of the project) or `custom` (use a catalog voice or your saved custom voice).

  **Default:** `cloning`
</ParamField>

<ParamField body="voiceId" type="string">
  Required when `voiceMode` is `custom`. Use an ID from `dubbing_voices_list`: a catalog voice or a custom voice saved by the authenticated account.
  Team billing does not grant access to another member's saved voices.
</ParamField>

<ParamField body="voiceIsolatorOption" type="string">
  Voice isolation mode when `keepBackgroundMusic` is enabled: `studio` or `realistic`.

  **Default:** `studio`
</ParamField>

<ParamField body="subtitles" type="boolean">
  Burn subtitles into the output video. Requires a video project.

  **Default:** `false`
</ParamField>

<ParamField body="subtitlesSource" type="string">
  Subtitle text when `subtitles` is enabled: `translated` (your SRT) or `original` (the source transcript).

  **Default:** `translated`
</ParamField>

<ParamField body="lipSyncMode" type="string">
  Run lip-sync once the dub is ready: `standard`, `pro` or `studio`. Plan, duration and credit rules are the same as on
  [Start Translation](/docs/api-reference/translate). Requires a video project.

  **Default:** omit the field to skip lip-sync.
</ParamField>

<ParamField body="activeSpeakerDetectionEnabled" type="boolean">
  Enable active-speaker detection for the requested lip-sync run.

  **Default:** `false`
</ParamField>

<ParamField body="voiceCloningSettings" type="object">
  Voice cloning fine-tuning, as a JSON object with `stability` and `similarity` between `0` and `1`. Only
  with `voiceMode: "cloning"`. See [Start Translation](/docs/api-reference/translate) for guidance on each value.
</ParamField>

<ParamField body="translationTimeSkips" type="array">
  Source-time ranges to leave in the original language, as `[{ "startTime": 10.2, "endTime": 12.8 }]`. Requires a video
  project and `keepBackgroundMusic: true`, cannot be combined with `keepOriginalVoice`, and cannot overlap transcript segments.
</ParamField>

## Response

<ResponseField name="success" type="boolean" required>
  Always `true` for successful requests.
</ResponseField>

<ResponseField name="message" type="string" required>
  A human-readable confirmation.
</ResponseField>

<ResponseField name="projectId" type="string" required>
  The project being dubbed.
</ResponseField>

<ResponseField name="workId" type="string" required>
  The translation work used for `targetLanguage`. It matches `workId` in the status, details and transcript endpoints.
</ResponseField>

<ResponseField name="estimatedDuration" type="number" required>
  Rough processing estimate in minutes: five minutes of processing per started minute of source media.
</ResponseField>

```json Response theme={null}
{
  "success": true,
  "message": "Dubbing started successfully from the provided translation. Use the status endpoint to track progress.",
  "projectId": "project_123",
  "workId": "work_456",
  "estimatedDuration": 10
}
```

## Tracking the run

The project switches to the `translation` workflow as soon as the dub starts, so [Get Translation Status](/docs/api-reference/translation-status)
follows the new run: `status` moves from `processing` to `success` (with `translatedVideoUrl` and `translatedAudioUrl`)
or `failed` (with `error`). The `project.translation.completed` and `project.translation.failed` webhooks are sent with the
same `targetLanguage`. In [Get Project Details](/docs/api-reference/project-details), the translated version reports
`settings.dubbingMode: "dub-only"` and `settings.smartSync: false`.

Alignment failures appear after the POST has been accepted. The status endpoint returns `status: "failed"` with `error.code` and `error.message`; the `project.translation.failed` webhook carries the same code and message. `TRANSLATED_SRT_UNMATCHED_CUES` names unmatched cue numbers in the message, and `TRANSLATED_SRT_MISSING_SEGMENTS` names segment indexes without a cue in the message. These asynchronous responses do not include structured `details`. Fix the SRT and call the endpoint again: the failed version stays in the project history and a new one is created.

### Checking the pacing

Export the translated transcript as JSON with [Export Project Transcript](/docs/api-reference/project-transcript). Each segment
carries `finalSpeedFactor`, the ratio between the generated speech and the segment window. Shorten the cues above roughly
`1.3`, then run the dub again with the revised SRT.

## Example flow

A typical automation (for example an n8n or Make scenario) chains the endpoints as follows:

1. `POST /v1/projects` with the media, `targetLanguage` and an explicit `originalLanguage` (transcription only).
2. Poll `GET /v1/translate/{projectId}/status` until `transcriptionStatus` is `success`, or wait for the `project.created` webhook.
3. `POST /v1/projects/{projectId}/transcript` with `{ "source": "original", "outputFormat": "srt" }` and translate the cues,
   keeping the numbering and timecodes.
4. `POST /v1/projects/{projectId}/dub` with the translated SRT.
5. Poll the status endpoint until `status` is `success`, or wait for `project.translation.completed`.
6. Download `translatedVideoUrl` (or `lipSync.videoUrl` when lip-sync was requested).
7. `DELETE /v1/translate/{projectId}` once the outputs are stored on your side.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.voicecheap.ai/v1/projects/project_123/dub" \
    -H "x-api-key: vc_your-api-key" \
    -H "Content-Type: application/json" \
    -d '{
      "targetLanguage": "english",
      "translatedSrt": "1\n00:00:00,000 --> 00:00:02,400\nWelcome to this lesson.\n\n2\n00:00:02,400 --> 00:00:05,100\nToday we talk about fractions.",
      "keepBackgroundMusic": true,
      "subtitles": true,
      "subtitlesSource": "translated"
    }'
  ```

  ```typescript TypeScript theme={null}
  interface DubProjectResponse {
    success: boolean;
    message: string;
    projectId: string;
    workId: string;
    estimatedDuration: number;
  }

  const API_BASE_URL = 'https://api.voicecheap.ai/v1';
  const headers = { 'x-api-key': 'vc_your-api-key', 'Content-Type': 'application/json' };

  async function waitForStatus(projectId: string, isReady: (status: any) => boolean): Promise<any> {
    while (true) {
      const status = await fetch(`${API_BASE_URL}/translate/${projectId}/status`, { headers }).then((response) => response.json());
      if (status.status === 'failed') throw new Error(`${status.error?.code}: ${status.error?.message}`);
      if (isReady(status)) return status;
      await new Promise((resolve) => setTimeout(resolve, 15_000));
    }
  }

  async function dubExistingProject(projectId: string, translatedSrt: string): Promise<string> {
    // 1. The project was created earlier with POST /v1/projects; wait for its transcript.
    await waitForStatus(projectId, (status) => status.transcriptionStatus === 'success');

    // 2. Send the translation you produced from the exported transcript.
    const dubResponse = await fetch(`${API_BASE_URL}/projects/${projectId}/dub`, {
      method: 'POST',
      headers,
      body: JSON.stringify({ targetLanguage: 'english', translatedSrt, keepBackgroundMusic: true }),
    });
    if (!dubResponse.ok) {
      const error = await dubResponse.json();
      throw new Error(`${error.code}: ${error.message}`);
    }
    const { workId } = (await dubResponse.json()) as DubProjectResponse;
    console.log('Dubbing started on work', workId);

    // 3. Wait for the dubbed output.
    const completed = await waitForStatus(projectId, (status) => status.status === 'success');
    return completed.translatedVideoUrl as string;
  }
  ```

  ```python Python theme={null}
  import time
  import requests

  API_BASE_URL = 'https://api.voicecheap.ai/v1'
  HEADERS = {'x-api-key': 'vc_your-api-key'}


  def wait_for_status(project_id, is_ready):
      while True:
          status = requests.get(f'{API_BASE_URL}/translate/{project_id}/status', headers=HEADERS).json()
          if status['status'] == 'failed':
              raise RuntimeError(f"{status['error']['code']}: {status['error']['message']}")
          if is_ready(status):
              return status
          time.sleep(15)


  def dub_existing_project(project_id, translated_srt):
      wait_for_status(project_id, lambda status: status['transcriptionStatus'] == 'success')

      response = requests.post(
          f'{API_BASE_URL}/projects/{project_id}/dub',
          headers=HEADERS,
          json={'targetLanguage': 'english', 'translatedSrt': translated_srt, 'keepBackgroundMusic': True},
      )
      response.raise_for_status()
      print('Dubbing started on work', response.json()['workId'])

      completed = wait_for_status(project_id, lambda status: status['status'] == 'success')
      return completed['translatedVideoUrl']
  ```
</CodeGroup>

## Errors

| Status | Code | Description |
| - | - | - |
| 400 | `VALIDATION_ERROR` | A body field is missing or invalid (`details` lists each field) |
| 400 | `INVALID_TRANSLATED_SRT` | translatedSrt does not contain valid SRT cues |
| 400 | `INVALID_BOOLEAN_VALUE` | A boolean option was not sent as a JSON boolean |
| 400 | `INVALID_NUMBER_VALUE` | A numeric option was not sent as a JSON number |
| 400 | `DURATION_DETECTION_FAILED` | The stored media duration of the project is unavailable |
| 400 | `VOICE_ID_REQUIRED` | Custom voice mode was selected without a voice ID |
| 400 | `VOICE_ID_NOT_ALLOWED_WITH_CLONING` | A voice ID was combined with voice cloning |
| 400 | `VOICE_CLONING_SETTINGS_NOT_ALLOWED` | Clone settings were combined with custom voice mode |
| 400 | `TRANSLATION_TIME_SKIP_CONFLICT` | Time skips conflict with another audio option |
| 400 | `TRANSLATION_TIME_SKIP_REQUIRES_BACKGROUND` | Time skips require background audio |
| 400 | `TRANSLATION_TIME_SKIP_OUT_OF_RANGE` | A time skip extends beyond the media duration |
| 400 | `SUBTITLES_NOT_AVAILABLE_FOR_AUDIO` | Burned-in subtitles were requested for an audio project |
| 400 | `LIPSYNC_NOT_AVAILABLE_FOR_AUDIO` | Lip sync was requested for an audio project |
| 400 | `TRANSLATION_TIME_SKIP_NOT_AVAILABLE_FOR_AUDIO` | Time skips were requested for an audio project |
| 400 | `LIPSYNC_VIDEO_TOO_LONG` | Lip-sync was requested for media longer than the supported duration |
| 401 | `MISSING_API_KEY` | API key is required |
| 401 | `INVALID_API_KEY_FORMAT` | API key must start with `vc_` |
| 401 | `INVALID_API_KEY` | The provided API key is invalid |
| 403 | `SUBSCRIPTION_REQUIRED` | API access requires a Starter plan or higher |
| 403 | `FORBIDDEN` | You do not have permission to access this project |
| 403 | `INSUFFICIENT_CREDITS` | Not enough credits to dub this project |
| 403 | `CUSTOM_VOICE_ACCESS_DENIED` | The selected voice is neither a catalog voice nor a custom voice saved by the authenticated account. |
| 403 | `LIPSYNC_MODE_ACCESS_DENIED` | The premium lip-sync mode is unavailable on the current plan |
| 404 | `PROJECT_NOT_FOUND` | The project does not exist |
| 409 | `DUB_ONLY_REQUIRES_TRANSCRIPTION` | The project transcription is still processing or failed |
| 409 | `TRANSLATION_ALREADY_IN_PROGRESS` | A dubbing run is already in progress for this language on the project |
| 429 | `RATE_LIMIT_EXCEEDED` | Too many requests (limit: 10 requests per minute) |
| 429 | `CONCURRENT_TRANSLATION_LIMIT_REACHED` | Account concurrency limit reached; see `details.limit` |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

Alignment failures detected while the run executes (`TRANSLATED_SRT_UNMATCHED_CUES`, `TRANSLATED_SRT_MISSING_SEGMENTS`) are
returned by the status endpoint and the failed webhook, not by this request.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.