> For the documentation index, fetch https://sync.so/docs/llms.txt. Append .md to a page URL for Markdown. Documentation-search MCP: https://sync.so/docs/_mcp/server.

# Voice Cloning

> Clone a voice from an audio or video sample and reuse it across text-to-speech and lip sync with the sync. labs Voices API.

The Voices API lets you list the voices available to your account, clone a new voice from an audio or video sample, and delete clones you no longer need. A cloned voice returns a `voiceId` that you reuse anywhere a voice is accepted — in [`POST /v2/tts`](/api-reference/api/tts-api/create) and in `text` inputs on [`POST /v2/generate`](/api-reference/api/generate-api/create).

The headline flow: clone a speaker's voice from a talking-head **video**, synthesize a brand-new line in that voice, then lip sync the result onto a different video — all on a single API key. See [The flagship flow](#the-flagship-flow) below.

## Listing voices

`GET /v2/voices` returns every voice available to you: sync. labs' built-in voices plus any clones you have created. Use a voice's `id` as the `voiceId` in [text-to-speech](/developer-guides/text-to-speech) and in generation `text` inputs.

**`curl`**

```bash curl
curl https://api.sync.so/v2/voices \
  -H "x-api-key: $SYNC_API_KEY"
```

**`list_voices.py`**

```python list_voices.py
from sync import Sync

sync = Sync()

voices = sync.voices.list()
for voice in voices:
    print(voice.id, voice.name, voice.provider)
```

**`list_voices.ts`**

```typescript list_voices.ts
import { SyncClient } from "@sync.so/sdk";

const sync = new SyncClient();

const voices = await sync.voices.list();
for (const voice of voices) {
    console.log(voice.id, voice.name, voice.provider);
}
```

The response is an array of voice objects:

```json
[
  {
    "id": "EXAVITQu4vr4xnSDxMaL",
    "name": "Rachel",
    "provider": "elevenlabs",
    "previewUrl": "https://assets.sync.so/voices/rachel-preview.mp3"
  }
]
```

**`id`** `string`

The voice identifier. Pass this as `voiceId` in [`POST /v2/tts`](/api-reference/api/tts-api/create) and in generation `text` inputs.

---

**`internalVoiceId`** `string`

sync. labs' internal identifier for the voice. Present on some voices; prefer `id` for API calls.

---

**`voiceId`** `string`

Provider-side voice identifier. Present on some voices.

---

**`name`** `string`

Human-readable voice name.

---

**`provider`** `string` — required

The voice provider. Always `"elevenlabs"`.

---

**`previewUrl`** `string`

A URL to a short audio preview of the voice, when available.

---

## Cloning a voice

`POST /v2/voices` clones a new voice from a sample and returns a `voiceId` you can use immediately. Provide a `name` plus **either** a sync. labs-hosted `url` **or** an `assetId` — not both.

> **Warning**
>
> The source sample must be hosted in sync. labs storage. Public third-party URLs are not accepted. Upload local files first with [`POST /v2/assets/upload`](/developer-guides/asset-uploads) and pass the returned `assetId`, or pass the `url` of an asset already in sync. labs storage.

Both **audio and video** sources are supported. For video sources, the audio track is extracted automatically and the first 2 minutes are used for cloning.

### Clone from an uploaded asset

The recommended path: upload the sample with the [Asset Uploads](/developer-guides/asset-uploads) flow, then clone from the returned `assetId`.

**`curl`**

```bash curl
curl -X POST https://api.sync.so/v2/voices \
  -H "x-api-key: $SYNC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Founder voice",
    "assetId": "asset_abc123"
  }'
```

**`clone_from_asset.py`**

```python clone_from_asset.py
from sync import Sync

sync = Sync()

# Upload a local sample to sync. labs storage first
upload = sync.assets.create_upload(...)
# ... PUT the file bytes to upload.url, then register the asset ...
asset = sync.assets.create(...)

voice = sync.voices.clone(
    name="Founder voice",
    asset_id=asset.id,
)

print(voice.voice_id)
```

**`clone_from_asset.ts`**

```typescript clone_from_asset.ts
import { SyncClient } from "@sync.so/sdk";

const sync = new SyncClient();

// Upload a local sample to sync. labs storage first
const upload = await sync.assets.createUpload(/* ... */);
// ... PUT the file bytes to upload.url, then register the asset ...
const asset = await sync.assets.create(/* ... */);

const voice = await sync.voices.clone({
    name: "Founder voice",
    assetId: asset.id,
});

console.log(voice.voiceId);
```

### Clone from a hosted URL

If your sample already lives in sync. labs storage, pass its `url` instead of an `assetId`.

**`curl`**

```bash curl
curl -X POST https://api.sync.so/v2/voices \
  -H "x-api-key: $SYNC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Founder voice",
    "url": "https://assets.sync.so/uploads/founder-sample.mp4"
  }'
```

**`clone_from_url.py`**

```python clone_from_url.py
from sync import Sync

sync = Sync()

voice = sync.voices.clone(
    name="Founder voice",
    url="https://assets.sync.so/uploads/founder-sample.mp4",
)

print(voice.voice_id)
```

**`clone_from_url.ts`**

```typescript clone_from_url.ts
import { SyncClient } from "@sync.so/sdk";

const sync = new SyncClient();

const voice = await sync.voices.clone({
    name: "Founder voice",
    url: "https://assets.sync.so/uploads/founder-sample.mp4",
});

console.log(voice.voiceId);
```

### Request body

**`name`** `string` — required

A label for the cloned voice.

---

**`url`** `string`

URL of an audio or video sample hosted in sync. labs storage. Provide either `url` or `assetId`, not both.

---

**`assetId`** `string`

ID of an asset previously uploaded via [`POST /v2/assets/upload`](/developer-guides/asset-uploads). Provide either `assetId` or `url`, not both.

---

### Response

A `201` response returns the new voice:

```json
{
  "voiceId": "cloned_9f8e7d6c",
  "name": "Founder voice",
  "id": "550e8400-e29b-41d4-a716-446655440000"
}
```

**`voiceId`** `string` — required

The cloned voice identifier. Use it as `voiceId` in [`POST /v2/tts`](/api-reference/api/tts-api/create) and in generation `text` inputs.

---

**`name`** `string` — required

The name you supplied for the clone.

---

**`id`** `string` — required

Sync's UUID for the clone. Use this ID to retrieve or delete the voice.

---

> **Note**
>
> Clone slots are limited by your plan. When you hit the limit, `POST /v2/voices` returns a `403`. Delete a voice you no longer need to free a slot, then retry the clone.

## Deleting a voice

`DELETE /v2/voices/{id}` removes a clone and **frees a clone slot**. Use the Sync UUID in the clone response's `id` field; `voiceId` identifies the provider voice.

**`curl`**

```bash curl
curl -X DELETE https://api.sync.so/v2/voices/550e8400-e29b-41d4-a716-446655440000 \
  -H "x-api-key: $SYNC_API_KEY"
```

**`delete_voice.py`**

```python delete_voice.py
from sync import Sync

sync = Sync()

sync.voices.delete("550e8400-e29b-41d4-a716-446655440000")
```

**`delete_voice.ts`**

```typescript delete_voice.ts
import { SyncClient } from "@sync.so/sdk";

const sync = new SyncClient();

await sync.voices.delete("550e8400-e29b-41d4-a716-446655440000");
```

A `200` response confirms the voice was deleted and the slot is available for a new clone.

## The flagship flow

Clone a voice from a talking-head **video**, synthesize a **new line** in that voice with text-to-speech, then lip sync that audio onto a **different** video. The entire pipeline runs on one API key.

#### Upload the source video (if it's a local file)

Voice sources must be hosted in sync. labs storage. If your talking-head video lives locally, upload it first with the [Asset Uploads](/developer-guides/asset-uploads) flow and keep the returned `assetId`. If it already lives in sync. labs storage, skip ahead and use its `url`.

#### Clone the voice from the video

Call `POST /v2/voices` with the `assetId` (or `url`). The audio track is extracted from the video automatically — the first 2 minutes are used — and you get back a `voiceId`.

```bash
curl -X POST https://api.sync.so/v2/voices \
  -H "x-api-key: $SYNC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Speaker clone",
    "assetId": "asset_talkinghead"
  }'
```

#### Synthesize a new line in the cloned voice

Pass the returned `voiceId` to [`POST /v2/tts`](/developer-guides/text-to-speech) to generate audio of a brand-new script in that voice.

```bash
curl -X POST https://api.sync.so/v2/tts \
  -H "x-api-key: $SYNC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "voiceId": "cloned_9f8e7d6c",
    "script": "Here is a brand new line, spoken in my own voice."
  }'
```

Poll the TTS job until it completes, then take the resulting `synthesizedAudioUrl`.

#### Lip sync the audio onto a different video

Send the synthesized audio and a **different** target video to `POST /v2/generate`. The target speaker's lips are driven by the cloned-voice audio.

```bash
curl -X POST https://api.sync.so/v2/generate \
  -H "x-api-key: $SYNC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "lipsync-2",
    "input": [
      { "type": "video", "url": "https://assets.sync.so/uploads/target-video.mp4" },
      { "type": "audio", "url": "https://assets.sync.so/tts/synthesized-line.wav" }
    ],
    "options": { "sync_mode": "cut_off" }
  }'
```

Poll `GET /v2/generate/{id}` until `status` is `COMPLETED`, then read `outputUrl`.

The end-to-end version of this pipeline in Python and TypeScript:

**`flagship_flow.py`**

```python flagship_flow.py
import time
from sync import Sync
from sync.common import Audio, GenerationOptions, Video

sync = Sync()

# 1. Clone the voice from a talking-head video already in sync. labs storage
voice = sync.voices.clone(
    name="Speaker clone",
    asset_id="asset_talkinghead",
)

# 2. Synthesize a new line in the cloned voice
tts = sync.tts.create(
    voice_id=voice.voice_id,
    script="Here is a brand new line, spoken in my own voice.",
)
synthesized_audio_url = tts.synthesized_audio_url

# 3. Lip sync that audio onto a different video
response = sync.generations.create(
    input=[
        Video(url="https://assets.sync.so/uploads/target-video.mp4"),
        Audio(url=synthesized_audio_url),
    ],
    model="lipsync-2",
    options=GenerationOptions(sync_mode="cut_off"),
)

job_id = response.id
generation = sync.generations.get(job_id)
while generation.status not in ["COMPLETED", "FAILED", "REJECTED"]:
    time.sleep(10)
    generation = sync.generations.get(job_id)

if generation.status == "COMPLETED":
    print(f"Video ready: {generation.output_url}")
```

**`flagship_flow.ts`**

```typescript flagship_flow.ts
import { SyncClient } from "@sync.so/sdk";

const sync = new SyncClient();

// 1. Clone the voice from a talking-head video already in sync. labs storage
const voice = await sync.voices.clone({
    name: "Speaker clone",
    assetId: "asset_talkinghead",
});

// 2. Synthesize a new line in the cloned voice
const tts = await sync.tts.create({
    voiceId: voice.voiceId,
    script: "Here is a brand new line, spoken in my own voice.",
});
const synthesizedAudioUrl = tts.synthesizedAudioUrl;

// 3. Lip sync that audio onto a different video
const response = await sync.generations.create({
    input: [
        { type: "video", url: "https://assets.sync.so/uploads/target-video.mp4" },
        { type: "audio", url: synthesizedAudioUrl },
    ],
    model: "lipsync-2",
    options: { sync_mode: "cut_off" },
});

let generation = await sync.generations.get(response.id);
while (!["COMPLETED", "FAILED", "REJECTED"].includes(generation.status)) {
    await new Promise((r) => setTimeout(r, 10000));
    generation = await sync.generations.get(response.id);
}

if (generation.status === "COMPLETED") {
    console.log(`Video ready: ${generation.outputUrl}`);
}
```

## FAQ

#### What sources can I clone from?

Audio and video samples hosted in sync. labs storage. For video, the audio track is extracted automatically and the first 2 minutes are used. Sources hosted outside sync. labs storage are not accepted — upload local files via [`POST /v2/assets/upload`](/developer-guides/asset-uploads) first and pass the returned `assetId`, or pass the `url` of an asset already in sync. labs storage.

#### Why did my clone return a 403?

Clone slots are limited per plan. A `403` from `POST /v2/voices` means you have reached your clone limit. Delete a voice you no longer need with `DELETE /v2/voices/{id}` to free a slot, then retry. Deleting a voice frees the slot immediately.

#### Where do I use the returned voiceId?

Anywhere a voice is accepted: as `voiceId` in [`POST /v2/tts`](/api-reference/api/tts-api/create) to synthesize speech, and in `text` inputs on [`POST /v2/generate`](/api-reference/api/generate-api/create). You can also retrieve it later from `GET /v2/voices`, where provider and internal voice identifiers are returned.

#### Do I pass both url and assetId?

No — provide exactly one. Use `assetId` when you have uploaded the sample through the [Asset Uploads](/developer-guides/asset-uploads) flow, or `url` when the sample already lives in sync. labs storage.

## Related

* [Text-to-Speech](/developer-guides/text-to-speech) — synthesize speech with a cloned `voiceId`.
* [Asset Uploads](/developer-guides/asset-uploads) — upload local audio or video into sync. labs storage before cloning.
* [Voices API reference](/api-reference/api/voices-api/list) — full request and response schemas for list, clone, and delete.