# Design a voice

`POST https://api.paxalabs.com/v1/voices/design`

POST /v1/voices/design makes a voice from a written description, or refines a draft with an instruction. It returns the voice's words, its example line as MP3, and a draft to refine or save.

Describe who is speaking and how they sound, such as their age, gender, pitch, pace, manner, and the language they speak. The voice speaks the language the description names, else the language the description is written in. Its name, role, and description come back in the language the description is written in.

To change the voice, send its `draft` back with an `instruction` such as "a little lower and slower". Every response carries a new draft, and a voice exists only once you [save](https://paxalabs.com/docs/api/save-voice) its draft.

Each call costs 5 credits, the example line included, charged before the work and refunded automatically when no voice comes back. A retry designs a new voice and is charged again. The endpoint refuses an [Idempotency-Key](https://paxalabs.com/docs/idempotency) with [400 validation](https://paxalabs.com/docs/errors#validation). A call takes a rate-limit token and one of the plan's [concurrent speech slots](https://paxalabs.com/docs/rate-limits#concurrency).

## Headers

- `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended).
- `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present.

See authentication at https://paxalabs.com/docs/authentication.

## Body

- `description` (string, optional, 1 to 1,000 characters, example "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai"): Who the voice is and how it sounds, in English, Thai, or Chinese, up to 1,000 characters: age, gender, pitch, pace, manner, and the language it speaks. The voice speaks the language this names, else the language this is written in, and its name, role, and description come back in the language this is written in.
- `draft` (string, optional, 1 to 4,096 characters): A draft an earlier design response returned, to refine that voice. Send it with instruction and without description.
- `instruction` (string, optional, 1 to 500 characters): What to change about the draft's voice, up to 500 characters, such as "a little lower and slower". The rest of the voice stays as it was.

## Response

- `draft` (string, required): The voice, sealed. Send it back with an instruction to refine the voice, or to POST /v1/voices/designed to save it. It opens only for this account.
- `voice` (object, required): The designed voice's words.
  - `voice.name` (string, required): A name for the voice, which saving keeps unless you send another.
  - `voice.role` (string, required): Who the voice is, in a few words.
  - `voice.description` (string, required): How the voice sounds, in one sentence.
  - `voice.gender` ("f" or "m", required): Voice gender, "f" or "m".
  - `voice.language` ("th" or "en" or "zh", required): The one language the voice speaks: "th" (Thai), "en" (English), or "zh" (Mandarin Chinese, Simplified script).
  - `voice.line` (string, required): The example line audio speaks, up to 120 characters in the voice's language.
  - `voice.model` (string, required): The model the voice speaks on, paxa-tts-flash-v2.
- `audio` (string, required): The example line spoken in the voice, as base64 MP3.
- `usage` (object, required): What the request was billed for.
  - `usage.credits` (number, required): What this turn cost, 5 credits.

## Errors

- `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list.
- `draft_invalid` (400): The draft field holds no draft this account received from POST /v1/voices/design. It is altered or cut short, belongs to another account, or no longer opens after a platform update. Nothing was charged.
- `unauthorized` (401): The request carried no API key, or the key is invalid or disabled.
- `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged.
- `key_limit` (403): This API key reached its spending cap. Nothing was charged.
- `content_blocked` (422): The upstream safety system declined to process this content. The charge was refunded.
- `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key.
- `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime.
- `internal` (500): Request state was inconsistent on the server.
- `provider_error` (502): Model inference failed after the request was charged.
- `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged.

## Example

curl:

```bash
curl -X POST https://api.paxalabs.com/v1/voices/design \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai"
  }' \
  -o design.json

jq -r .audio design.json | base64 --decode > line.mp3
```

TypeScript:

```typescript
import { writeFile } from "node:fs/promises";

const headers = {
  Authorization: `Bearer ${process.env.PAXA_API_KEY}`,
  "Content-Type": "application/json",
};

// Design, then refine the draft with an instruction.
let response = await fetch("https://api.paxalabs.com/v1/voices/design", {
  method: "POST",
  headers,
  body: JSON.stringify({
    description: "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai",
  }),
});
if (!response.ok) throw new Error(`Design failed: ${response.status}`);
let turn = await response.json();

response = await fetch("https://api.paxalabs.com/v1/voices/design", {
  method: "POST",
  headers,
  body: JSON.stringify({
    draft: turn.draft,
    instruction: "A little slower, with a softer ending",
  }),
});
if (!response.ok) throw new Error(`Refine failed: ${response.status}`);
turn = await response.json();
await writeFile("line.mp3", Buffer.from(turn.audio, "base64"));
```

Python:

```python
import base64
import os
import requests

URL = "https://api.paxalabs.com/v1/voices/design"
HEADERS = {"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"}

# Design, then refine the draft with an instruction.
turn = requests.post(
    URL,
    headers=HEADERS,
    json={"description": "A calm Bangkok pharmacist in her thirties who explains dosages slowly and kindly, speaking Thai"},
)
turn.raise_for_status()

turn = requests.post(
    URL,
    headers=HEADERS,
    json={
        "draft": turn.json()["draft"],
        "instruction": "A little slower, with a softer ending",
    },
)
turn.raise_for_status()

with open("line.mp3", "wb") as f:
    f.write(base64.b64decode(turn.json()["audio"]))
```

Response:

```json
{
  "draft": "kD3v9QxW2mN7pL0sT5yB8cR1fH4jK6zA...",
  "voice": {
    "name": "Nok",
    "role": "Pharmacist",
    "description": "A calm, warm voice that explains each step slowly.",
    "gender": "f",
    "language": "th",
    "line": "ทานยานี้หลังอาหารวันละสามครั้งนะคะ ถ้ามีผื่นขึ้นให้หยุดยาแล้วกลับมาหาเราได้เลยค่ะ",
    "model": "paxa-tts-flash-v2"
  },
  "audio": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYy...",
  "usage": {
    "credits": 5
  }
}
```
