> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

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

<Note>
  Every endpoint on this page requires
  `Authorization: Bearer sk_live_YOUR_API_KEY`, except a signed preview URL.
  Resources are scoped to the organization associated with the credential.
</Note>

## 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                |
| `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

```json theme={null}
{
  "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`, `female`, or an empty string when omitted                |
| `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.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/voices/policy \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

```json Response (200 OK) theme={null}
{
  "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.

<CodeGroup>
  ```bash cURL theme={null}
  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"
    }'
  ```
</CodeGroup>

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

```json Response (201 Created) theme={null}
{
  "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

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/voices/quota-requests/current \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

```json Response (200 OK) theme={null}
{
  "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.

<CodeGroup>
  ```bash cURL theme={null}
  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"
  ```
</CodeGroup>

| 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  | no       | `male` or `female`                                                                                 |

```json Response (202 Accepted) theme={null}
{
  "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`](/webhooks/events#voice-events) 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 must be male or female."}`                                       | Gender is present but 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:

```json theme={null}
{
  "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:

```json theme={null}
{
  "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:

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/voices/clones \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

```json Response (200 OK) theme={null}
[
  {
    "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](#custom-voice-object), or `404 Not Found` if the
voice does not exist, belongs to another organization, or was deleted.

## Rename a clone

Only `display_name` can be changed. The agent-facing `name`,
`custom:<public_id>`, stays stable.

<CodeGroup>
  ```bash cURL theme={null}
  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"}'
  ```
</CodeGroup>

Returns the updated [custom voice object](#custom-voice-object).

| Status | Response                                                                | When                                         |
| ------ | ----------------------------------------------------------------------- | -------------------------------------------- |
| `400`  | `{"detail":"Only display_name may be changed."}`                        | The body contains any other field            |
| `400`  | `{"display_name":"Display name must be between 1 and 100 characters."}` | Display name is empty, absent, or too long   |
| `404`  | Not found                                                               | The clone is unavailable to the organization |

## Delete a clone

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE \
    https://api.thunderphone.com/v1/voices/clones/cv_2f6f90b0e9a34ee8b39be7d1 \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

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:

<CodeGroup>
  ```bash cURL theme={null}
  curl \
    https://api.thunderphone.com/v1/voices/clones/cv_2f6f90b0e9a34ee8b39be7d1/preview \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    --output preview.mp3
  ```
</CodeGroup>

An 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:

<CodeGroup>
  ```bash cURL theme={null}
  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"}'
  ```
</CodeGroup>

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`](/api-reference/agents#voices):

```json theme={null}
{
  "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`.
