# 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
- **Max duration:** 10 minutes
- **Max file size:** 500 MB
- **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](/content/docs/dubbing-api#overview/index.html). 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 48 hours.

### 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](/content/docs/dubbing-api#upload-media-optional/index.html) 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.

```plaintext
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` Try it

#### Request Body

JSON schema Example request:

```json
{
  "filename": "string",
  "content_type": "string"
}
```

```json
{
  "filename": "video.mp4",
  "content_type": "video/mp4"
}
```

**Parameters:**

| Field          | Type   | Required | Description                                |
|----------------|--------|----------|--------------------------------------------|
| `filename`     | string | Yes      | Name of the file (e.g., "video.mp4")    |
| `content_type` | string | Yes      | MIME type of the file (e.g., "video/mp4")|

#### Response

200 JSON schema Example response:

```json
{
  "upload_url": "string",
  "source_url": "string",
  "upload_id": "string",
  "max_file_size_bytes": 0,
  "expires_in_sec": 0
}
```

### Create Dubbing Job

Submit an audio or video file for dubbing. The file will be processed asynchronously.

POST `/api/dubbing/jobs` Try it

#### Request Body

JSON schema Example request:

```json
{
  "source_url": "string",
  "target_lang": "string",
  "source_lang": "string",
  "reduce_accent": "boolean",
  "translation_lag_time": "number",
  "original_speech_volume": "number"
}
```

```json
{
  "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     |
| `target_lang`            | string    | Yes      | Target language code                       |
| `source_lang`            | string    | No       | Source language code or "auto"          |
| `reduce_accent`          | boolean   | No       | Reduce source language accent in dubbed audio. When set to True, produces more natural target language pronunciation.|  
| `translation_lag_time`   | number    | No       | Seconds to delay translated speech        |
| `original_speech_volume`  | number    | No       | Mix original speech back in at this volume.|

#### Response (201 Created)

201 JSON schema Example response:

```json
{
  "job_id": "string",
  "status": "string",
  "source_lang": "string",
  "target_lang": "string",
  "created_at": "string",
  "limits": {
    "max_duration_sec": 0,
    "max_file_size_bytes": 0
  }
}
```

### Get Job Status

Poll this endpoint to track the progress of a dubbing job.

GET `/api/dubbing/jobs/{id}` Try it

#### Response

200 JSON schema Example response:

```json
{
  "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"
}
```

### List Jobs

Retrieve a paginated list of your dubbing jobs.

GET `/api/dubbing/jobs?limit=20&offset=0` Try it

#### Query Parameters

| Parameter | Type   | Required | Description                       |
|-----------|--------|----------|-----------------------------------|
| `limit`   | number | No       | Number of jobs to return          |
| `offset`  | number | No       | Offset for pagination             |

#### Response

200 JSON schema Example response:

```json
{
  "jobs": [...],
  "total": 5,
  "limit": 20,
  "offset": 0
}
```

### Get Download URL (refresh)

Generates a fresh presigned download URL for a completed dubbing job.

GET `/api/dubbing/jobs/{id}/result` Try it

#### Response

200 JSON schema Example response:

```json
{
  "job_id": "string",
  "download_url": "string",
  "expires_at": "string"
}
```

## Status Flow

Jobs progress through these statuses:

| Status | Description                                                |
|--------|------------------------------------------------------------|
| `pending` | Job created, waiting to start                           |
| `downloading` | Downloading the source media                           |
| `processing` | Dubbing in progress                                   |
| `uploading` | Uploading the dubbed output                             |
| `completed` | Done — `output_url` is available                      |
| `failed` | An error occurred — check `error` field                  |

## Error Codes

400 401 402 429

```json
// invalid_url
{
  "error": {
    "code": "invalid_url",
    "message": "The provided source URL is not a valid URL"
  }
}
```

```json
{
"error": {
"code": "unauthorized",
"message": "Invalid or expired API token"
  }
}
```

### Job-Level Errors

| Error Code           | Description 
|----------------------|-----------------------------------------------------|
| `video_too_long`    | Input exceeds 10 minute limit                       |
| `video_too_short`   | Input is less than 1 second                         |
| `video_too_large`   | File size exceeds 500 MB limit                       |
| `unsupported_format` | Media format or codec is not supported              |
| `download_failed`    | Could not download the source media from the provided URL |
| `processing_failed`  | An internal error occurred during dubbing           |

## Example: Full Workflow

```javascript
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);
  }
}
```
