---
title: "Generate a voiceover from text (ElevenLabs), paid with x402"
method: POST
path: "/api/x402/speech"
---

# Generate a voiceover from text (ElevenLabs), paid with x402

`POST /api/x402/speech`

## Request

- Base URL: `https://www.trezalabs.com`
- URL: `https://www.trezalabs.com/api/x402/speech`
- Auth: HTTP bearer

Speaks up to 3000 characters of text in one of over 30 ElevenLabs voices and returns an MP3, usually within seconds. Priced per character ($0.42 per 1,000, rounded up to the cent, minimum $0.02); the 402 quotes the exact figure for your text. Pay via the x402 protocol (HTTP 402): call without payment to receive a 402 challenge carrying the price and networks, sign the payment with an x402-capable wallet, and retry with the payment-signature header. The challenge lists two ways to pay the same amount: USDC on Base (eip155:8453) first, then USDC on Solana mainnet (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp); a client pays the first one it has a wallet for, and neither needs gas money in the paying wallet. No account, API key, or signup is involved, so the payment is the only credential. A payment presented again after it was redeemed is refused with a 409 (code PAYMENT_ALREADY_REDEEMED). Only the documented fields are read; a request carrying any other field is refused with a 400 before any payment moves.

The paid call answers 200 at once, with the statusUrl in the X-Status-Url header, then sends a whitespace byte every 2 seconds while it waits (JSON parsers ignore it), and ends with one complete JSON object. Read status in that body: success carries audio, the URL of the speech; error means it failed, nothing was charged for it, and retryUrl tries again on the wallet balance the payment left, without paying twice; running means it outlasted the wait (45 seconds by default, Prefer: wait=N for up to 720), so poll statusUrl. Send "async": true or Prefer: respond-async for an immediate answer (200, status running) with a statusUrl. Lost the statusUrl? GET /api/x402/purchases and sign its challenge with the paying wallet (Sign-In-With-X, free) to list every purchase with its file and a fresh statusUrl. And the same request sent again from the same wallet, once the first render has finished and its file never reached you, is answered with that file (alreadyPaid, paidUsd 0) and the new payment is not taken.

## Headers

- `Prefer` string

## Request body

- object
  - `text` string, required: What to say.
  - `voice` string: A voices[].id from GET /api/x402/speech, which lists every voice with what it sounds like: ElevenLabs' default voices and the library voices on our account, over 30 in all, e.g. george (warm UK storyteller), brian (deep US narrator), alice (clear UK educator). A voice whose menu entry carries a priceMultiplier costs that multiple of the per-character price, and the 402 quotes it. An unknown voice is refused with a 400 before any payment moves.
  - `model` 'eleven-v3' | 'eleven-multilingual-v2': eleven-v3: the most expressive read, and performs inline directions in square brackets ([whispers], [excited]) without speaking them. eleven-multilingual-v2: the steadiest read for long narration, 29 languages.
  - `async` boolean: true answers at once (200, status running) with a statusUrl to poll.

## Response `200`

Payment settled and the response waited for the speech. Streamed: the status and headers are sent at once (X-Status-Url carries the statusUrl), then a whitespace byte every 2 seconds, then one complete JSON object. The HTTP status is 200 whatever happened, so read status in the body.

- object
  - `runId` string
  - `status` 'success' | 'error' | 'partial' | 'cancelled' | 'running': success carries audio. error, partial, and cancelled carry the failure fields. running means it is still going; poll statusUrl.
  - `startedAt` string, date-time
  - `finishedAt` string, date-time
  - `durationMs` integer
  - `audio` string, uri: URL of the finished speech. Use it exactly as given: its query parameters, including c=, are part of it.
  - `statusUrl` string, uri: The run's status, now and later. It carries its own signed claim ticket, so it needs no other credential. Use it exactly as given: the runId contains '#', encoded as %23.
  - `pollAfterMs` integer: Only when status is running: how long to wait before polling statusUrl.
  - `alreadyPaid` object: Only when this wallet already bought this exact request and its file never reached it: the earlier purchase this answer hands over (runId, boughtAt). The new payment was not taken and paidUsd is 0.
    - `runId` string
    - `boughtAt` string, date-time
  - `paidUsd` number: What this request cost.
  - `model` string
  - `voice` string
  - `characters` integer: Characters spoken: what the price was computed from.
  - `network` string: Where the payment settled: eip155:8453 (Base) or solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp (Solana).
  - `transaction` string: The settled payment: a 0x transaction hash on Base, a base58 transaction signature on Solana.
  - `payer` string: The paying wallet.
  - `error` string: Failed runs only. What went wrong, in the provider's words.
  - `reason` 'prompt_refused' | 'render_failed': Failed runs only. prompt_refused: the model declined the text, so reword it. render_failed: it did not finish; the same text may succeed.
  - `retryable` boolean: Failed runs only. True until the run's one free retry has been used.
  - `retryUrl` string, uri: Failed runs only. POST here to try again on the wallet balance, optionally with {"text": "..."} in the body. Carries the same signed ticket.
  - `message` string: Failed runs only. What happened and what to do, in one sentence.
  - `balanceUsd` number: Failed runs only. The wallet balance the retry spends.

## Other responses

- `400`: Missing or oversized text, a voice or model that is not on the menu, or a field this endpoint does not read. Refused before settlement, so nothing was charged.
- `402`: Payment required: the challenge response carrying x402 payment instructions
- `409`: This payment was already redeemed (code PAYMENT_ALREADY_REDEEMED). Nothing new was generated or charged; send a new payment to buy another.
- `429`: Rate limit exceeded
- `503`: Pay-per-call speech is not enabled on this deployment, or is temporarily unavailable.

---

[API](https://skmtc.dev/trezalabs/apis/treza-pipelines-api.md) · [All operations](https://skmtc.dev/trezalabs/apis/treza-pipelines-api/llms.txt) · [OpenAPI document](https://skmtc.dev/trezalabs/apis/treza-pipelines-api/revisions/a76574c1731b?raw)
