> 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. > 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.