# Quickstart

> Dub an English video into Spanish, from token to downloaded MP4, with curl, Python or Node.js.

By the end of this page you will have uploaded a video, had it dubbed into Spanish with voices
cloned from its own speakers, and downloaded an MP4 that carries both the Spanish dub and the
original audio.

## Get access

VOD Express comes with every SyncWords Live account: if your organization uses Live, it can use
VOD Express too, with the same **API key pair** (a public key and a secret). Without one, ask your
SyncWords contact, or [support@syncwords.com](/vod-express/support), for an account.

## You need

- The key pair, and about **15 minutes**: processing a file takes **4 to 10 minutes** even for a
  short clip, most of it spent preparing the processing for the file.
- A short video with speech. Use ours, a 35-second English dialogue (1 MB):
  [`sample-en.mp4`](/vod-express/samples/sample-en.mp4); or any MP4 of a minute or two.

  ```bash
  curl -s -o episode.mp4 https://docs.syncwords.com/vod-express/samples/sample-en.mp4
  ```
- One of:
  - `curl` and [`jq`](https://jqlang.org)
  - Python 3.9+ with [`requests`](https://requests.readthedocs.io) (`pip install requests`)
  - Node.js 20+

Each step's snippet builds on the ones before it: run the curl snippets in one shell, and put the
Python or Node.js snippets one after another in one file (`quickstart.py`, or `quickstart.mjs` for
its top-level `await`). The base URL is read from `SW_VOD_API` when set, and is
that of [Environments](/vod-express/environments) otherwise.

### 1. Get a token

Exchange the key pair for a bearer token, and set up a small helper for the calls that follow:

```bash cURL
API=${SW_VOD_API:-https://api-vod-express.syncwords.com/v1}

TOKEN=$(curl -s "$API/token/create" \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"username": "YOUR_PUBLIC_KEY", "password": "YOUR_SECRET_KEY"}' | jq -r .access_token)
AUTH="Authorization: Bearer $TOKEN"
```

```python Python
import os
import time

import requests

API = os.environ.get("SW_VOD_API", "https://api-vod-express.syncwords.com/v1")

response = requests.post(
  API + "/token/create",
  json={"username": "YOUR_PUBLIC_KEY", "password": "YOUR_SECRET_KEY"},
  headers={"Accept": "application/json"},
)
response.raise_for_status()
token = response.json()["access_token"]

def api(method, path, body=None):
  response = requests.request(
      method, API + path, json=body,
      headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
  )
  if not response.ok:
      raise RuntimeError(f"{method} {path}: {response.status_code} {response.text}")
  return response.json()["data"]
```

```javascript Node.js
import { readFile, stat, writeFile } from 'node:fs/promises';

const API = process.env.SW_VOD_API ?? 'https://api-vod-express.syncwords.com/v1';

const response = await fetch(`${API}/token/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ username: 'YOUR_PUBLIC_KEY', password: 'YOUR_SECRET_KEY' }),
});
if (!response.ok) throw new Error(`token: ${response.status} ${await response.text()}`);
const { access_token: token } = await response.json();

async function api(method, path, body) {
const response = await fetch(API + path, {
  method,
  headers: {
    Authorization: `Bearer ${token}`,
    Accept: 'application/json',
    ...(body ? { 'Content-Type': 'application/json' } : {}),
  },
  body: body ? JSON.stringify(body) : undefined,
});
const json = await response.json();
if (!response.ok) throw new Error(`${method} ${path}: ${response.status} ${JSON.stringify(json)}`);
return json.data;
}
```

A token lasts 30 minutes. When a call answers `401`, get a new one the same way.
[More on tokens](/vod-express/authentication).

### 2. Check your access

```bash cURL
curl -s -H "$AUTH" $API/whoami | jq .data.vodExpress
```

```python Python
print(api("GET", "/whoami")["vodExpress"])
```

```javascript Node.js
console.log((await api('GET', '/whoami')).vodExpress);
```

```json
{ "enabled": true, "defaultBuildVersion": "0.9.0-alpha1-2026-10-05T14-57Z-bf4a93", "hasAwsAccess": false, "canManageAccount": true }
```

`enabled: false` means VOD Express is not available to the account, for instance because its
access was revoked or its trial ended: every other call then answers `403`, and its `message`
says why. Ask [support](/vod-express/support).

### 3. Create a project

A project holds the processing instructions. This one reads the files put into its input
folder (`S3FolderSource`), transcribes English, translates into Spanish with Gemini, dubs with
Deepdub cloning each speaker's voice (`voice: auto`), and writes an MP4 per input into its
output folder (`S3FolderOutput`), with the input's own audio as the first track and the Spanish
dub as the second:

```bash cURL
PROJECT=$(curl -s -X POST $API/projects -H "$AUTH" -H 'Content-Type: application/json' -d '{
"name": "My first dub",
"config": {
  "sourceMedia": { "@type": "S3FolderSource" },
  "transcription": { "@type": "Speechmatics", "language": "en" },
  "translations": [
    {
      "language": "es",
      "engine": { "@type": "Gemini" },
      "ttsOptions": { "provider": { "@type": "Deepdub", "voice": "auto" } }
    }
  ],
  "outputs": [
    {
      "@type": "S3FolderOutput",
      "items": [
        {
          "@type": "VideoFile",
          "ext": "mp4",
          "nameTemplate": "${name}.es.${ext}",
          "useOriginalTracks": true,
          "dubTracks": [{ "language": "es", "title": "Español" }]
        }
      ]
    }
  ]
}
}' | jq -r .data.id)
echo $PROJECT
```

```python Python
project = api("POST", "/projects", {
  "name": "My first dub",
  "config": {
      "sourceMedia": {"@type": "S3FolderSource"},
      "transcription": {"@type": "Speechmatics", "language": "en"},
      "translations": [
          {
              "language": "es",
              "engine": {"@type": "Gemini"},
              "ttsOptions": {"provider": {"@type": "Deepdub", "voice": "auto"}},
          }
      ],
      "outputs": [
          {
              "@type": "S3FolderOutput",
              "items": [
                  {
                      "@type": "VideoFile",
                      "ext": "mp4",
                      "nameTemplate": "${name}.es.${ext}",
                      "useOriginalTracks": True,
                      "dubTracks": [{"language": "es", "title": "Español"}],
                  }
              ],
          }
      ],
  },
})["id"]
print(project)
```

```javascript Node.js
const project = (
await api('POST', '/projects', {
  name: 'My first dub',
  config: {
    sourceMedia: { '@type': 'S3FolderSource' },
    transcription: { '@type': 'Speechmatics', language: 'en' },
    translations: [
      {
        language: 'es',
        engine: { '@type': 'Gemini' },
        ttsOptions: { provider: { '@type': 'Deepdub', voice: 'auto' } },
      },
    ],
    outputs: [
      {
        '@type': 'S3FolderOutput',
        items: [
          {
            '@type': 'VideoFile',
            ext: 'mp4',
            nameTemplate: '${name}.es.${ext}',
            useOriginalTracks: true,
            dubTracks: [{ language: 'es', title: 'Español' }],
          },
        ],
      },
    ],
  },
})
).id;
console.log(project);
```

`${name}` and `${ext}` in `nameTemplate` are the API's placeholders, filled per input file, so
the Node.js snippet keeps them in single quotes rather than a template literal.

The project starts right away (`"status": "running"`) and waits for files. If the config has a
mistake, the answer is a `422` listing every problem by its place in the config. See
[Errors](/vod-express/guides/errors).

### 4. Upload a video

Ask the API for an upload URL, then `PUT` the file to it. The bytes go straight to storage,
and the file is in the project as soon as the `PUT` completes:

```bash cURL
FILE=episode.mp4
URL=$(curl -s -X POST $API/projects/$PROJECT/uploads -H "$AUTH" -H 'Content-Type: application/json' \
-d "{\"name\": \"$FILE\", \"size\": $(wc -c < "$FILE" | tr -d ' ')}" | jq -r .data.url)
curl -T "$FILE" "$URL"
```

```python Python
file = "episode.mp4"
url = api("POST", f"/projects/{project}/uploads", {"name": file, "size": os.path.getsize(file)})["url"]
with open(file, "rb") as handle:
  requests.put(url, data=handle).raise_for_status()
```

```javascript Node.js
const file = 'episode.mp4';
const { url } = await api('POST', `/projects/${project}/uploads`, { name: file, size: (await stat(file)).size });
const put = await fetch(url, { method: 'PUT', body: await readFile(file) });
if (!put.ok) throw new Error(`upload: S3 answered ${put.status}`);
```

Storage answers `200` with an empty body. The URL works for a file of up to 5 GB; a larger
file, or an upload you want to resume after an interruption, goes
[in parts](/vod-express/uploads/api-uploads#parts).

> **Tip:**
> Moving many files, or files from a pipeline? Use the AWS CLI with the role the API vends
> for your account: `aws s3 sync ./season-1 s3://…/input/season-1/`. See
> [Upload with the AWS CLI](/vod-express/uploads/aws-role).

### 5. Wait for it

```bash cURL
curl -s -H "$AUTH" $API/projects/$PROJECT/files | jq '.data[] | {name, status, outputs}'
```

```python Python
for f in api("GET", f"/projects/{project}/files"):
  print(f["name"], f["status"], f["outputs"])
```

```javascript Node.js
for (const f of await api('GET', `/projects/${project}/files`)) {
console.log(f.name, f.status, f.outputs);
}
```

```json
{
  "name": "episode.mp4",
  "status": "pending",
  "outputs": [{ "path": "episode.es.mp4", "available": false, "size": null }]
}
```

A file goes from `pending` to `processing` to `done` (or `failed`). This is the 4 to 10 minutes
of waiting, and most of it reads `pending`: `processing` is only the last step. To wait in a loop:

```bash cURL
until curl -s -H "$AUTH" $API/projects/$PROJECT/files \
| jq -e 'all(.data[]; .status == "done" or .status == "failed")' > /dev/null; do sleep 30; done
```

```python Python
while not all(f["status"] in ("done", "failed") for f in api("GET", f"/projects/{project}/files")):
  time.sleep(30)
```

```javascript Node.js
while (!(await api('GET', `/projects/${project}/files`)).every((f) => ['done', 'failed'].includes(f.status))) {
await new Promise((resolve) => setTimeout(resolve, 30_000));
}
```

A token lasts 30 minutes, so a long wait may need a new one: the loop in
[Monitoring](/vod-express/guides/monitoring#a-good-polling-loop) renews it on `401`.

### 6. Download the dub

```bash cURL
URL=$(curl -s -H "$AUTH" "$API/projects/$PROJECT/outputs/download?path=episode.es.mp4" | jq -r .data.url)
curl -s -o episode.es.mp4 "$URL"
```

```python Python
url = api("GET", f"/projects/{project}/outputs/download?path=episode.es.mp4")["url"]
with requests.get(url, stream=True) as response, open("episode.es.mp4", "wb") as out:
  response.raise_for_status()
  for chunk in response.iter_content(1 << 20):
      out.write(chunk)
```

```javascript Node.js
const { url } = await api('GET', `/projects/${project}/outputs/download?path=episode.es.mp4`);
const download = await fetch(url);
if (!download.ok) throw new Error(`download: S3 answered ${download.status}`);
await writeFile('episode.es.mp4', Buffer.from(await download.arrayBuffer()));
```

The link is a presigned S3 URL, valid for an hour. Open `episode.es.mp4` in any player: it
opens on the original audio, and the player's audio menu switches to the Spanish dub.

## What happened

- The project launched a processing service when you created it. It picked up `episode.mp4` from
  the project's input folder, transcribed it, translated each sentence, cloned each speaker's
  voice for the Spanish lines, and mixed the new voices over the original background (the voices
  of the original are removed, because `originalAudioSplitting` is on by default).
- The service **stops itself** 30 minutes after it has nothing left to do, and starts again on
  its own within a minute of your next upload (`startOnNewFiles`, on by default). See
  [Auto-stop](/vod-express/concepts/projects#auto-stop).
- Every further file you upload is processed with the same configuration.

## Next steps

- **[More languages, more outputs](/vod-express/examples/multi-language)**: Several dubs in one video, per-language files, subtitles beside them.
- **[Pick voices](/vod-express/configuration/voices)**: A safe first voice for each provider, and how to try one before a season.
- **[Automate it](/vod-express/examples/automation)**: A complete Python or Node.js client: token renewal, a folder of files, every output.
- **[Upload a whole season](/vod-express/examples/season-batch)**: The AWS CLI, subfolders, and collecting every output.
