Custom voices
Request access, clone a voice, track its lifecycle, and use it on an agent.
Custom voices let your organization create a reusable agent voice from a
short audio sample. Creation is asynchronous: a new clone starts in queued,
moves to processing, and finishes as either ready or failed.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v1/voices/policy | Check access and ThunderPhone quota |
POST | /v1/voices/quota-requests | Request access or a larger quota |
GET | /v1/voices/quota-requests/current | Retrieve the latest quota request |
GET | /v1/voices/clones | List custom voices |
POST | /v1/voices/clones | Create a custom voice |
GET | /v1/voices/clones/{public_id} | Retrieve a custom voice |
PATCH | /v1/voices/clones/{public_id} | Rename a custom voice or set its gender |
DELETE | /v1/voices/clones/{public_id} | Delete a custom voice |
GET | /v1/voices/clones/{public_id}/preview | Stream the ready voice's MP3 preview |
Custom voice object
{
"name": "custom:cv_2f6f90b0e9a34ee8b39be7d1",
"public_id": "cv_2f6f90b0e9a34ee8b39be7d1",
"display_name": "Support voice",
"language": "en",
"gender": "female",
"status": "ready",
"failure_reason": "",
"created_at": "2026-07-30T14:12:08.317Z",
"sample_url": "https://api.thunderphone.com/v1/voices/clones/cv_2f6f90b0e9a34ee8b39be7d1/preview?sig=..."
}| Field | Type | Description |
|---|---|---|
name | string | Agent voice value in the form custom:<public_id> |
public_id | string | Public identifier used in clone endpoints |
display_name | string | Organization-facing name, 1–100 characters |
language | string | The clone's single language code |
gender | string | male or female. Voices created before gender became required may return an empty string until it is set with PATCH |
status | string | queued, processing, ready, or failed |
failure_reason | string | Processing failure detail; empty unless the clone failed |
created_at | timestamp | ISO 8601 creation time |
sample_url | string | null | Signed preview URL when a preview is available; otherwise null |
Request access and quota
Check policy
Check whether cloning is enabled and how many of your organization's ThunderPhone custom voice slots are in use.
curl https://api.thunderphone.com/v1/voices/policy \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"cloning_enabled": true,
"max_custom_voices": 5,
"used": 2,
"custom_voice_surcharge_cents": 1
}| Field | Type | Description |
|---|---|---|
cloning_enabled | boolean | Whether the organization can create custom voices |
max_custom_voices | integer | ThunderPhone custom voice limit for the organization |
used | integer | Non-deleted custom voices currently using that limit |
custom_voice_surcharge_cents | integer | Additional cents per minute when a custom voice is used |
Submit a quota request
Use the same flow to request initial access or a larger ThunderPhone limit. Only one request can be pending for an organization at a time.
curl -X POST https://api.thunderphone.com/v1/voices/quota-requests \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"requested_count": 5,
"note": "Voices for our support teams"
}'| Field | Type | Required | Description |
|---|---|---|---|
requested_count | integer | yes | Requested ThunderPhone custom voice limit; at least 1 |
note | string | no | Context for the request, up to 2,000 characters |
{
"status": "pending",
"requested_count": 5,
"created_at": "2026-07-30T13:55:42.104Z"
}| Status | Response | When |
|---|---|---|
400 | {"requested_count":"Must be an integer."} | Count is absent or not an integer |
400 | {"requested_count":"Must be at least 1."} | Count is less than 1 |
400 | {"note":"Must be 2000 characters or fewer."} | Note is too long |
409 | {"detail":"A quota request is already pending."} | The organization already has a pending request |
Check the latest request
curl https://api.thunderphone.com/v1/voices/quota-requests/current \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"status": "approved",
"requested_count": 5,
"created_at": "2026-07-30T13:55:42.104Z",
"resolved_at": "2026-07-30T14:03:19.880Z"
}| Field | Type | Description |
|---|---|---|
status | string | pending, approved, or denied |
requested_count | integer | Requested ThunderPhone custom voice limit |
created_at | timestamp | ISO 8601 submission time |
resolved_at | timestamp | null | ISO 8601 decision time, or null while pending |
Returns 404 Not Found when the organization has never submitted a request.
Create a custom voice
Upload one WAV or MP3 file as multipart/form-data. The sample must contain
3–15 seconds of speech and be no larger than 4 MB. Use one language per
clone, and only upload a voice you have the rights and consent to clone.
curl -X POST https://api.thunderphone.com/v1/voices/clones \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-F "audio=@support-voice.wav;type=audio/wav" \
-F "name=Support voice" \
-F "language=en" \
-F "gender=female" \
-F "consent=true"| Field | Type | Required | Description |
|---|---|---|---|
audio | file | yes | WAV or MP3, no larger than 4 MB, containing 3–15 seconds of speech |
name | string | yes | Display name, 1–100 characters |
language | string | yes | One of ar, zh, nl, en, fr, de, he, hi, it, ja, ko, pl, pt, ru, or es |
consent | boolean | yes | Must be exactly true to confirm the right and consent to clone the voice |
gender | string | yes | male or female. ThunderPhone tells the agent's language models which gender the voice has so replies use the matching first-person forms in gendered languages (Spanish, Russian, Hindi, ...) |
{
"name": "custom:cv_2f6f90b0e9a34ee8b39be7d1",
"public_id": "cv_2f6f90b0e9a34ee8b39be7d1",
"display_name": "Support voice",
"language": "en",
"gender": "female",
"status": "queued",
"failure_reason": "",
"created_at": "2026-07-30T14:12:08.317Z",
"sample_url": null
}The 202 Accepted response means the upload was queued, not that the voice is
ready. Poll the clone detail until it reaches a terminal status, or subscribe
to voice.ready and voice.failed for push
notifications.
| Status | Code or response | When |
|---|---|---|
400 | {"language":"Unsupported custom voice language."} | Language is missing or unsupported |
400 | {"audio":"An audio file is required."} | Audio part is missing |
400 | {"audio":"Audio must be a WAV or MP3 file."} | Filename extension and MIME type are not an accepted WAV or MP3 combination |
400 | {"audio":"Audio must be 4 MB or smaller."} | File exceeds 4 MB |
400 | {"audio":"Audio must not be empty."} | File is empty |
400 | {"consent":"Consent must be exactly true to confirm rights to clone this voice."} | Consent is not exactly true |
400 | {"name":"Name is required."} | Name is empty or absent |
400 | {"name":"Name must be 100 characters or fewer."} | Name is too long |
400 | {"gender":"Gender is required and must be male or female."} | Gender is missing or unsupported |
403 | cloning_not_enabled | Custom voice cloning is not enabled for the organization |
422 | custom_voice_quota_exceeded | The organization's ThunderPhone custom voice limit is already in use |
The 403 response is:
{
"code": "cloning_not_enabled",
"detail": "Custom voice cloning is not enabled. Submit a quota request to request access."
}The quota response includes current ThunderPhone usage and limit:
{
"code": "custom_voice_quota_exceeded",
"detail": "Custom voice quota exceeded.",
"current": 5,
"max": 5
}List and retrieve clones
List all non-deleted custom voices for the organization, newest first:
curl https://api.thunderphone.com/v1/voices/clones \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"[
{
"name": "custom:cv_2f6f90b0e9a34ee8b39be7d1",
"public_id": "cv_2f6f90b0e9a34ee8b39be7d1",
"display_name": "Support voice",
"language": "en",
"gender": "female",
"status": "ready",
"failure_reason": "",
"created_at": "2026-07-30T14:12:08.317Z",
"sample_url": "https://api.thunderphone.com/v1/voices/clones/cv_2f6f90b0e9a34ee8b39be7d1/preview?sig=..."
}
]Retrieve one clone with GET /v1/voices/clones/{public_id}. It returns the
same custom voice object, or 404 Not Found if the
voice does not exist, belongs to another organization, or was deleted.
Rename a clone or set its gender
display_name and gender can be changed; send either or both. The
agent-facing name, custom:<public_id>, stays stable. Use gender to
backfill clones created before it became required.
curl -X PATCH \
https://api.thunderphone.com/v1/voices/clones/cv_2f6f90b0e9a34ee8b39be7d1 \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"display_name":"Billing voice","gender":"female"}'Returns the updated custom voice object.
| Status | Response | When |
|---|---|---|
400 | {"detail":"Only display_name and gender may be changed."} | The body is empty or contains any other field |
400 | {"display_name":"Display name must be between 1 and 100 characters."} | display_name is present but empty or too long |
400 | {"gender":"Gender must be male or female."} | gender is present but unsupported |
404 | Not found | The clone is unavailable to the organization |
Delete a clone
curl -X DELETE \
https://api.thunderphone.com/v1/voices/clones/cv_2f6f90b0e9a34ee8b39be7d1 \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"Returns 204 No Content. Deletion is soft: the voice disappears from clone
and catalog responses immediately and frees one ThunderPhone custom voice
slot. A deleted voice can no longer be assigned to an agent.
Play the preview
A ready clone's sample_url is a signed URL to:
GET /v1/voices/clones/{public_id}/preview?sig={signature}
It streams audio/mpeg without an Authorization header. You can also call
the endpoint without sig while authenticated:
curl \
https://api.thunderphone.com/v1/voices/clones/cv_2f6f90b0e9a34ee8b39be7d1/preview \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
--output preview.mp3An unsigned, unauthenticated request—or one with an invalid or expired
signature—returns 401 Unauthorized. A valid request returns 404 Not Found
when the clone is not ready or has no stored preview.
Use a custom voice
Once status is ready, pass the clone's name to an agent's voice or
draft_voice field:
curl -X PATCH https://api.thunderphone.com/v1/agents/42 \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"voice":"custom:cv_2f6f90b0e9a34ee8b39be7d1"}'The clone must be ready and owned by the agent's organization. A clone from another organization, a non-ready clone, or a deleted clone is rejected.
Ready custom voices also appear at the start of the voices array returned
by GET /v1/voices:
{
"name": "custom:cv_2f6f90b0e9a34ee8b39be7d1",
"display_name": "Billing voice",
"description": "",
"gender": "female",
"category": "custom",
"accent": null,
"languages": ["en"],
"custom": true,
"surcharges": {"en": 1},
"sample_url": "https://api.thunderphone.com/v1/voices/clones/cv_2f6f90b0e9a34ee8b39be7d1/preview?sig=..."
}The surcharges object is empty when the organization has no custom voice
surcharge. You can filter the catalog with ?category=custom, ?languages=en,
or ?gender=female.