Docs · Voice
Voice
Text to speech on the same key and under the same governance as every other request. One endpoint, a body with a single field, and a company configuration that decides everything else. For how the pieces fit together, start at the docs.
What it is
A request that turns text into audio, through the gateway you already call. Your existing Selan key is the only credential it needs.
The request carries the text and nothing else. Which model speaks,
which voice it speaks in, what format the audio comes back as and how much text may
go in one request are your company's configuration, held at the gateway. A body that
names its own model, voice or response_format
is refused with 400 rather than quietly honoured.
The caller does not choose a provider and never holds a provider key, so switching the voice, paying for it and auditing its use all happen in one place rather than in every script that speaks.
Make a request
POST /openai/v1/audio/speech against the gateway, carrying your Selan key
as a bearer exactly as the other routes do:
curl https://gw.selan.ai/openai/v1/audio/speech \
-H "Authorization: Bearer $SELAN_API_KEY" \
-H "content-type: application/json" \
-d '{"input": "The text to speak."}' \
-o speech.mp3
The body is that one field:
{"input": "The text to speak."}
What comes back is the audio itself — raw bytes, a Content-Type of
audio/mpeg, and an X-Generation-Id header naming the
upstream generation. There is no JSON envelope to unwrap and no URL to fetch
afterwards, so -o is the whole of saving it. Add -D - to see
the headers beside it.
/v1/audio/speech is served as well, for a client that builds its path
without the /openai prefix. It behaves identically.
When it refuses
| Status | What it means |
|---|---|
200 |
The audio, as bytes. |
400 |
Empty input, input past maxInputCharacters, or a body trying to set its own model, voice or format. |
401 |
The token is missing or not valid. |
403 |
Your company has no voice configuration, or its key cannot serve the model that configuration names. |
502 |
The provider refused, or the audio came back over the gateway's ceiling. |
403 is the ordinary answer for a company that has not switched
voice on. Nothing is broken and there is nothing to retry — the
configuration is absent, and it stays absent until an owner sets it. Read it as
“voice is off here”, and see turning it on.
Turning it on
An owner sets a voice object on the company configuration. It is
null by default, and null means voice is off. There is no
separate on/off flag beside it: the configuration being there is the
enablement, and taking it away is how voice is switched off again.
| Field | What it sets |
|---|---|
model |
The voice model, named as a provider, a colon and that provider's own id — openrouter:hexgrad/kokoro-82m. |
voice |
The named voice it speaks in — af_heart. |
responseFormat |
mp3 or pcm16. |
maxInputCharacters |
The ceiling on a single request's input. Past it the request is refused rather than trimmed to fit, so you never get half a sentence read aloud. |
One configuration per company, not one per caller. A request cannot name a model or a voice, so everyone at your company speaks in the same one. That is what makes it a decision an owner makes once, rather than a field that drifts apart across every service that speaks.
What it costs
A voice request shows up in Logs beside every other request, on the same key, counting against the same budget. Speech is billed per character of the text you send, so what you pay for is the input rather than the length of the audio that comes back.
The price is the provider's own figure rather than something we work out from a rate table. Speech providers report no token count, so the Tokens column stays empty rather than showing a zero.
The cost appears about a minute after the request, not with it. The provider does not state what a request cost until after the audio has already reached you, so a fresh voice row shows a dash in the Cost column. A dash there means the price has not arrived yet.
The Claude Code plugin
selan-voice reads Claude's last reply out loud. It is public and MIT, so what it sends is readable before you install it:
/plugin marketplace add selan-ai/selan-voice
/plugin install selan-voice@selan-voice
It needs curl and awk, and nothing else. No
node, no python, no jq. Two variables configure it, and on a session that is already
authenticated neither usually has to be set:
| Variable | Effect |
|---|---|
SELAN_API_KEY |
The key it speaks with. If it is not set, ANTHROPIC_AUTH_TOKEN is used instead. |
SELAN_VOICE_URL |
The gateway to send to. Defaults to https://gw.selan.ai. |
A reply is written to be read rather than heard, so the markdown comes out before anything is spoken: a code block becomes the word “code”, a link becomes its own text, and a table is dropped.
speak --print shows you what would be spoken and spends nothing, which
is the cheap way to see what the stripping did. Above roughly thirty seconds of
estimated audio it reports the estimate first, rather than starting a monologue you
then have to sit through.