curl -X POST "https://api.hooked.so/v1/voice/clone" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: clone-founder-voice-1" \
-d '{
"name": "Founder voice",
"audioUrl": "https://cdn.example.com/voices/founder-sample.mp3",
"consent": true,
"language": "Spanish",
"gender": "female",
"accent": "Castilian",
"description": "Warm, calm narration"
}'
const response = await fetch("https://api.hooked.so/v1/voice/clone", {
method: "POST",
headers: {
"x-api-key": process.env.HOOKED_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "clone-founder-voice-1",
},
body: JSON.stringify({
name: "Founder voice",
audioUrl: "https://cdn.example.com/voices/founder-sample.mp3",
consent: true,
language: "Spanish",
gender: "female",
}),
});
const { data: voice } = await response.json();
// Use voice.id as voiceId in any create endpoint
console.log(voice.id, voice.name);
import os
import requests
response = requests.post(
"https://api.hooked.so/v1/voice/clone",
headers={"x-api-key": os.environ["HOOKED_API_KEY"], "Idempotency-Key": "clone-founder-voice-1"},
json={
"name": "Founder voice",
"audioUrl": "https://cdn.example.com/voices/founder-sample.mp3",
"consent": True,
"language": "Spanish",
"gender": "female",
},
)
voice = response.json()["data"]
print(voice["id"], voice["name"])
{
"success": true,
"message": "Voice cloned",
"data": {
"id": "cm4xa1b2c0001ab12cd34ef56",
"voiceId": "Xb7hH8MSUJpSbSDYk0k2",
"name": "Founder voice",
"gender": "female",
"language": "Spanish",
"accent": "Castilian",
"country": "ES",
"age": "middle_aged",
"templateUrl": "team/public/voice/cm4xa1b2c0001ab12cd34ef56.mp3",
"thumbnail": "/images/voice-default.png",
"isCustom": true,
"source": "custom",
"ownerProvider": "managed",
"description": "Warm, calm narration"
}
}
{
"success": false,
"code": "limit_reached",
"message": "You've reached your custom voice limit (1). Upgrade your plan to create more voices.",
"limit": 1,
"currentCount": 1
}
Clone a Voice
Clone a voice from a recording and use it in any narrated video
curl -X POST "https://api.hooked.so/v1/voice/clone" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: clone-founder-voice-1" \
-d '{
"name": "Founder voice",
"audioUrl": "https://cdn.example.com/voices/founder-sample.mp3",
"consent": true,
"language": "Spanish",
"gender": "female",
"accent": "Castilian",
"description": "Warm, calm narration"
}'
const response = await fetch("https://api.hooked.so/v1/voice/clone", {
method: "POST",
headers: {
"x-api-key": process.env.HOOKED_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "clone-founder-voice-1",
},
body: JSON.stringify({
name: "Founder voice",
audioUrl: "https://cdn.example.com/voices/founder-sample.mp3",
consent: true,
language: "Spanish",
gender: "female",
}),
});
const { data: voice } = await response.json();
// Use voice.id as voiceId in any create endpoint
console.log(voice.id, voice.name);
import os
import requests
response = requests.post(
"https://api.hooked.so/v1/voice/clone",
headers={"x-api-key": os.environ["HOOKED_API_KEY"], "Idempotency-Key": "clone-founder-voice-1"},
json={
"name": "Founder voice",
"audioUrl": "https://cdn.example.com/voices/founder-sample.mp3",
"consent": True,
"language": "Spanish",
"gender": "female",
},
)
voice = response.json()["data"]
print(voice["id"], voice["name"])
{
"success": true,
"message": "Voice cloned",
"data": {
"id": "cm4xa1b2c0001ab12cd34ef56",
"voiceId": "Xb7hH8MSUJpSbSDYk0k2",
"name": "Founder voice",
"gender": "female",
"language": "Spanish",
"accent": "Castilian",
"country": "ES",
"age": "middle_aged",
"templateUrl": "team/public/voice/cm4xa1b2c0001ab12cd34ef56.mp3",
"thumbnail": "/images/voice-default.png",
"isCustom": true,
"source": "custom",
"ownerProvider": "managed",
"description": "Warm, calm narration"
}
}
{
"success": false,
"code": "limit_reached",
"message": "You've reached your custom voice limit (1). Upgrade your plan to create more voices.",
"limit": 1,
"currentCount": 1
}
Overview
Creates a voice of your own from a recording, the same way Voice cloning works in the dashboard. Send a publichttps link to the sample in audioUrl. The answer is the new voice: pass its id as voiceId in any create endpoint, in Generate Speech or in Speech to Speech. It also shows up in List Voices with isCustom: true.
- The sample is downloaded. Local, private-network and plain
httpaddresses are refused with400, and every redirect is checked the same way. - The voice is cloned and saved to your team, with the language, gender, age, use case and accent you give (they label the voice in the dashboard;
languagealso sets the accents you can pick). - The voice records a short introduction in its own language. That sample becomes its
templateUrl.
consent must be true. By sending it you confirm you have the rights to upload and clone this voice, as the dashboard asks before cloning.403, code: "byok"). Voices in that account appear in List Voices and can be used as voiceId right away.curl -X POST "https://api.hooked.so/v1/voice/clone" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: clone-founder-voice-1" \
-d '{
"name": "Founder voice",
"audioUrl": "https://cdn.example.com/voices/founder-sample.mp3",
"consent": true,
"language": "Spanish",
"gender": "female",
"accent": "Castilian",
"description": "Warm, calm narration"
}'
const response = await fetch("https://api.hooked.so/v1/voice/clone", {
method: "POST",
headers: {
"x-api-key": process.env.HOOKED_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "clone-founder-voice-1",
},
body: JSON.stringify({
name: "Founder voice",
audioUrl: "https://cdn.example.com/voices/founder-sample.mp3",
consent: true,
language: "Spanish",
gender: "female",
}),
});
const { data: voice } = await response.json();
// Use voice.id as voiceId in any create endpoint
console.log(voice.id, voice.name);
import os
import requests
response = requests.post(
"https://api.hooked.so/v1/voice/clone",
headers={"x-api-key": os.environ["HOOKED_API_KEY"], "Idempotency-Key": "clone-founder-voice-1"},
json={
"name": "Founder voice",
"audioUrl": "https://cdn.example.com/voices/founder-sample.mp3",
"consent": True,
"language": "Spanish",
"gender": "female",
},
)
voice = response.json()["data"]
print(voice["id"], voice["name"])
{
"success": true,
"message": "Voice cloned",
"data": {
"id": "cm4xa1b2c0001ab12cd34ef56",
"voiceId": "Xb7hH8MSUJpSbSDYk0k2",
"name": "Founder voice",
"gender": "female",
"language": "Spanish",
"accent": "Castilian",
"country": "ES",
"age": "middle_aged",
"templateUrl": "team/public/voice/cm4xa1b2c0001ab12cd34ef56.mp3",
"thumbnail": "/images/voice-default.png",
"isCustom": true,
"source": "custom",
"ownerProvider": "managed",
"description": "Warm, calm narration"
}
}
{
"success": false,
"code": "limit_reached",
"message": "You've reached your custom voice limit (1). Upgrade your plan to create more voices.",
"limit": 1,
"currentCount": 1
}
Errors
| Status | Message / code | Cause |
|---|---|---|
| 400 | Invalid JSON body | The body is not JSON. |
| 400 | consent: Must be true: ... | consent is missing or not true. |
| 400 | name: Must be at least 2 characters | name is shorter than 2 or longer than 100 characters. |
| 400 | audioUrl: Must be a public https URL | Not a URL, not https, or a local or private address. |
| 400 | audioUrl: The file could not be downloaded (HTTP 404) | The link does not answer with the file. |
| 400 | audioUrl: Not an audio file (...) | The link is a page, an image, a PDF… |
| 400 | audioUrl: The file is over 10 MB | The sample is too large. |
| 400 | audioUrl: The audio is too short / too long | Shorter than 5 or longer than 90 seconds. |
| 400 | language: ..., accent: ... | A value the dashboard does not offer; the message lists the valid ones. |
| 401 | code: "not_authenticated" | Missing or invalid x-api-key. |
| 402 | code: "subscription_required" | Managed team without a live plan. |
| 402 | code: "missing_credentials" | Your team’s ElevenLabs key is missing. |
| 403 | code: "byok" | Your team uses its own ElevenLabs account: clone the voice there. |
| 403 | code: "limit_reached" | Your plan’s custom voices are used up. |
| 500 | Failed to clone the voice | The voice provider refused the sample or failed. Nothing is charged. |
Idempotency-Key header so a retry after a timeout answers with the first voice instead of cloning it twice.Authorizations
Headers
Makes the request safe to retry. 1-255 printable ASCII characters, one per operation (your job id, or a UUID you store), reused on every retry. Within 24 hours the same key with the same body answers with the first response and the header Idempotent-Replayed: true, without running or charging again. Keys are scoped to your team and the endpoint. 5xx answers and refusals before anything ran (401, 402, 403, 409, 429) are not kept. See Idempotency.
1 - 255^[\x20-\x7E]+$Body
The voice's name in your library.
2 - 100Public https link to the sample file itself: MP3, WAV, M4A, AAC, OGG, FLAC or WEBM, 5 to 90 seconds, up to 10 MB.
2000Must be true: you confirm you have the necessary rights to upload and clone this voice.
true A note about the voice, returned in List Voices.
500The sample's language, a name such as English, Spanish or Portuguese (case-insensitive; the dashboard's list of 55 languages).
Case-insensitive. Stored as Unknown when omitted.
male, female Case-insensitive. Stored as unknown when omitted.
young, middle_aged, old narrative_story, informative_educational, conversational, advertisement, social_media, entertainment_tv, characters_animation, general One of the accents the dashboard offers for language (for English: Standard, Neutral, American, British, Australian, ...). Case-insensitive; another value answers 400 with the list.