Text to Speech
Use the text-to-speech API to turn up to 50 texts into audio in one call. Every text becomes its own clip, voiced with the provider, model and voice you choose, and each clip comes back as a time-limited download link. It is built for prompts, announcements, IVR menus, notification audio and anything else you want pre-rendered rather than spoken live on a call.
Rendering is asynchronous. A submission returns a batch id immediately, and polling that id is the only way to collect the audio. There is no webhook, no callback, no batch list and no cancel endpoint.
How it works
Submit
POST /v1/tts/batch with your texts, provider and voice. You get back an id and status: "pending", and the exact charge for the batch is held on your balance.
Store the returned id, and collect the audio within an hour of submitting. A batch is only reachable by its id, and the id stops resolving one hour after submission.
Base URLs and authentication
Use the global production API unless your workspace is provisioned in the India region. A batch id is only resolvable on the deployment it was created on, so poll the same base URL you submitted to.
Every request needs an API key as a bearer token. Keys are created in the dashboard under Developer -> API Keys and are shown once.
A missing header, a scheme other than Bearer, an empty token, an unknown key, and a disabled key all return the same 401 authentication_error. A key that is not bound to a workspace is also refused with 401.
Every accepted batch spends text-to-speech credit. Keep keys server-side and rotate one that has been exposed.
Endpoints
Both are JSON.
Submit a batch
Content type: application/json.
Everything that can be checked without synthesizing audio is checked at submit, and nothing is billed when it fails: the text lengths, the provider, the model, the language, your workspace’s connection to the provider, your rate and your balance. A wrong voice_id is the exception — only the provider can tell, so it surfaces per clip when the batch is polled.
Poll a batch
A batch belongs to the workspace that submitted it. Polling another workspace’s id returns 404, exactly as an unknown id does.
Providers
tts_provider picks who voices the batch. The connection behind the provider is resolved server-side from your own workspace’s connections, so the API takes no connection id and never takes a provider key from you.
A provider your workspace has no connection to is rejected at submit with 400 invalid_request (no <provider> synthesizer connection is available to this tenant). An unknown provider is rejected with 400 as well. Nothing is billed either way.
Models
tts_model must be selectable for the provider you chose. Omit it and the provider’s first model is used.
A model that is not selectable for the chosen provider is rejected at submit with 400 invalid_request, and the message names the models that are.
Voices
voice_id is whatever the provider calls a voice, passed through unchanged, so it must belong to the provider you selected. One batch uses one voice; to voice texts in different voices, submit a batch per voice.
- ElevenLabs — ElevenLabs voice ids such as
21m00Tcm4TlvDq8ikWAM, from the ElevenLabs account behind your workspace’s connection. Every ElevenLabs voice works with every selectable ElevenLabs model. - Sarvam — voices are tied to a model. With
bulbul:v3, the 37 speakers listed in the podcast Voice catalogue, such aspriya,neha,adityaandrahul. Withbulbul:v4-flash, the 222 persona voices listed there, named<name>_<language>_<style>, such asritu_hi_customer. A voice sent with the other model fails every clip. - Smallest — the 249
lightning_v3.1_provoices listed in the podcast Voice catalogue, such asmandar,jasleenandaarini. - Vakyam — Vakyam voice names, such as
Archana. Voice names are case-sensitive.
The same lists are in the dashboard under an agent’s Voice tab, where you can preview a voice before using it.
A wrong voice_id is not caught at submit. Each clip fails when the provider rejects it, with the provider’s message in that clip’s error. When every clip fails, the batch is failed and the whole hold is refunded.
Language
language accepts a language name or a locale code, in any case: Hindi, hindi, hi and hi-IN all mean Hindi. It is normalized at submit to the locale code the providers expect — English and the Indic languages get the -IN region (hindi → hi-IN, english → en-IN), and a region you send yourself is kept (en-US, pt-BR). An unrecognized value is rejected with 400 invalid_request before anything is rendered or billed.
The code is passed to the provider with every clip. When you omit it, each provider falls back to its own default:
ElevenLabs is not sent a language code; it speaks the language the text is written in.
The text is spoken as written: it is not translated. Write each text in the language you ask for, and pick a voice that covers it — nothing cross-checks the three.
Audio format
The provider decides the container. audio_format in the poll response tells you which one every clip in the batch uses.
Clips are stored as audio/mpeg or audio/wav, so they play directly in a browser or an <audio> element. Transcode on your side if you need another format, μ-law or 8 kHz telephony audio.
Batch lifecycle
Clips succeed or fail independently: one rejected text does not throw away the clips that rendered. Up to four clips render at once, so a large batch takes roughly a quarter of the time it would one by one.
There is no cancel endpoint and no retry. A batch whose rendering is interrupted — a server restart, for instance — stops resolving, and polling it returns 404. Enforce your own deadline, and contact support with the batch id rather than resubmitting blindly, since its credit hold may still be held.
Recommended polling
Poll every 2 to 5 seconds. Each clip takes about as long to render as a provider call, so a batch of short texts finishes in seconds and a full batch of long texts in a minute or two.
The audio URL
Each audio_url is a signed link to the clip in private storage, valid for 900 seconds (15 minutes) from the moment that poll response was built.
- Fetch it without an
Authorizationheader. The signature is the credential; adding a bearer token is unnecessary and can break the request. - It plays inline. Storage serves range requests, so a player can seek.
- It is a bearer capability. Anyone holding the link can download the clip until it expires. Do not paste it into shared logs or tickets.
- When it expires, poll again. Every poll of a finished batch mints fresh links, for up to an hour after submission.
Download promptly, or store the bytes yourself. There is no permanent public URL.
Billing
Text to speech is billed per 1,000 characters at your workspace’s text-to-speech rate, set by FormantAI on your billing account. Characters are counted exactly as sent, spaces and punctuation included, with no rounding up to the next thousand.
- At submit, the full charge for every text in the batch is held on your balance: total characters ÷ 1,000 × rate, rounded to two decimal places. If the balance cannot cover it, the submission is refused with
402 insufficient_creditsand nothing is rendered. - When the batch finishes, it is settled against the clips that rendered. A clip that failed is refunded; a
failedbatch is refunded in full.
For example, at ₹30 per 1,000 characters, three texts totalling 58 characters hold and charge ₹1.74.
A workspace with no text-to-speech rate cannot use this API: submissions are refused with 400 invalid_request (billing account has no tts_service_rate_per_1k_chars) rather than voiced for free. Ask support to enable text to speech for your workspace.
Each batch holds its own credit, so batches submitted in parallel each need their own hold to be available.
Limits
A field outside its bounds is rejected with 422 before anything is checked or billed, except an over-long entry in texts, which is rejected with 400.
Rate limiting
Rate limiting is per workspace, defaulting to 50 requests per minute, and the bucket is shared with every other endpoint on this API — calls, batch calls, conversations, transcription and podcasts draw from the same budget as submitting and polling batches. See Errors & Rate Limits for the headers.
Errors
Every error except 422 uses the standard envelope:
A 422 comes from request parsing and uses FastAPI’s own shape instead, with a detail array and no type:
Per-clip errors are not HTTP errors: they arrive in a 200 poll response, in clips[].error. A provider rejection reads like sarvam tts failed (400) [speaker=... model=... lang=... text='...']: <provider message>, naming the voice, model, language and the start of the text. An error on our side reads internal error.
Retry 429 after Retry-After, and 503 with bounded exponential backoff. Do not retry 400, 401, 402, 404 or 422 until the request itself is fixed.
