selan.ai

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

StatusWhat 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.

FieldWhat 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:

VariableEffect
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.