---
title: "Voice samples"
description: "Generate a spoken sample of any voice from your own text."
---

Voice samples turn a short piece of your own text into speech in any
ThunderPhone voice — any [catalog voice](/api-reference/voices) or any of
your organization's ready [custom voices](/api-reference/custom-voices).
Use them to audition voices, preview a greeting before an agent goes live,
or let your users hear a voice say their own words.

Samples are meant for exactly that — sampling. Text is capped at 500
characters and generation is rate limited per organization. If you need
longer samples or a higher limit, contact support@thunderphone.com.

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

## Endpoints

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/v1/voices/preview` | Generate a sample and get a signed audio URL |
| `GET` | `/v1/voices/previews/{id}` | Stream a generated sample's MP3 |

## Generate a sample

<CodeGroup>
```bash cURL
curl -X POST https://api.thunderphone.com/v1/voices/preview \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "voice": "sarah",
    "language": "en",
    "text": "Thanks for calling Northwind Dental. How can I help you today?"
  }'
```
</CodeGroup>

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `voice` | string | yes | A voice `name` from [`GET /v1/voices`](/api-reference/agents#voices) — a catalog voice like `sarah`, or one of your ready custom voices like `custom:cv_2f6f90b0e9a34ee8b39be7d1` |
| `language` | string | yes | Language code for the sample. For a custom voice, this must be the clone's language |
| `text` | string | yes | The text to speak, 1–500 characters |

```json Response (200 OK)
{
  "id": "vp_9c2f90b0e9a34ee8b39be7d1",
  "url": "https://api.thunderphone.com/v1/voices/previews/vp_9c2f90b0e9a34ee8b39be7d1?sig=...",
  "cached": false,
  "charged_cents": 3,
  "expires_at": "2026-08-07T14:12:08.317Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Identifier of the generated sample |
| `url` | string | Signed URL that streams the sample's MP3 without authentication |
| `cached` | boolean | `true` when the sample was served from cache instead of freshly generated |
| `charged_cents` | integer | What this request cost: `3` for a fresh generation, `0` for a cache hit |
| `expires_at` | timestamp | ISO 8601 time the signed URL stops working |

## Pricing and caching

A fresh generation costs **3¢** from your prepaid balance. Repeating an
identical request — same voice, language, and text — is served from cache,
costs nothing, and returns `"cached": true` with a fresh signed URL. That
also means expired URLs are cheap to replace: re-send the same request and
you get a new one free.

Generation is limited to 30 fresh samples per organization per hour; cache
hits don't count against the limit.

## Fetch the audio

The `url` in the response is a signed link to:

`GET /v1/voices/previews/{id}?sig={signature}`

It streams `audio/mpeg` without an Authorization header, so you can hand it
straight to an `<audio>` element or a media player. Signed URLs are valid
for 7 days. You can also call the endpoint without `sig` while
authenticated:

<CodeGroup>
```bash cURL
curl \
  https://api.thunderphone.com/v1/voices/previews/vp_9c2f90b0e9a34ee8b39be7d1 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  --output sample.mp3
```
</CodeGroup>

An unsigned, unauthenticated request — or one with an invalid or expired
signature — returns `401 Unauthorized`. A sample generated for another
organization's custom voice returns `404 Not Found`.

## Errors

| Status | Code or response | When |
|--------|------------------|------|
| `400` | `{"voice":"Voice is required."}` | Voice is missing |
| `400` | `{"language":"Language is required."}` | Language is missing |
| `400` | `{"text":"Text must be between 1 and 500 characters."}` | Text is empty or too long |
| `400` | `{"voice":"Voice is not available."}` | Custom voice doesn't exist, isn't ready, or belongs to another organization |
| `400` | `{"language":"Language is not available for this voice."}` | The voice doesn't speak that language |
| `402` | `insufficient_balance` | Prepaid balance is below the sample price |
| `422` | `preview_unavailable_language` | Sample generation isn't available for this voice in that language yet |
| `429` | `preview_rate_limited` | The organization used its 30 fresh generations this hour |

The `402`, `422`, and `429` responses share one shape:

```json
{
  "code": "preview_rate_limited",
  "detail": "Sample generation limit reached. Try again later."
}
```

A `422` means the voice exists but ThunderPhone can't generate ad-hoc
samples for it in that language yet — the voice still works normally on
calls. Pick another voice or language for the sample.
