Batch
Send a line of text, get one complete audio body back. The OpenAI SDK works unchanged.
Use batch when the whole line is already written and a moment of silence before playback is fine — a confirmation, a voicemail, an audio file you are going to store. If a listener is waiting on the first word, you want streaming instead.
Call it
curl -X POST https://api.speko.ai/v1/audio/speech \
-H "Authorization: Bearer $SPEKO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "model": "auto", "input": "Your appointment is confirmed for Friday.", "response_format": "wav" }' \
--output confirmation.wavimport os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["SPEKO_API_KEY"],
base_url="https://api.speko.ai/v1",
)
speech = client.audio.speech.create(
model="auto",
input="Your appointment is confirmed for Friday.",
response_format="wav",
)
with open("confirmation.wav", "wb") as audio:
audio.write(speech.read())import fs from 'node:fs/promises';
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.SPEKO_API_KEY,
baseURL: 'https://api.speko.ai/v1',
});
const speech = await client.audio.speech.create({
model: 'auto',
input: 'Your appointment is confirmed for Friday.',
response_format: 'wav',
});
await fs.writeFile('confirmation.wav', Buffer.from(await speech.arrayBuffer()));model: "auto" hands the choice to the router: it ranks the live board for your key's language and objective, then calls the winner. Pin a model instead by passing its id from GET /v1/models.
Two endpoints, one behaviour
| Endpoint | Body | Use when |
|---|---|---|
/v1/audio/speech | JSON with input and model | You are using an OpenAI SDK. |
/v1/synthesize | JSON with text | You prefer the native name, or you are not going through an SDK. |
Both route identically. Every other field is shared:
| Field | Meaning |
|---|---|
voice | Provider voice id. Omit it and the router uses the chosen provider's validated default. |
language | BCP 47. An accent tag such as es-PR narrows to the voices that actually produce it — see languages and accents. |
speed | Honored on the providers that accept it. |
instructions | Delivery direction — style or accent, without changing the transcript. Gemini TTS applies it; the others ignore it. |
response_format | pcm, pcm16, or wav. Defaults to pcm. |
Audio format
| Value | You get |
|---|---|
pcm, pcm16 | Raw 24 kHz mono 16-bit signed little-endian PCM. No container, no header — what a telephony or WebRTC pipeline wants. |
wav | The same PCM behind a 44-byte RIFF header, so a player opens it directly. |
Every provider is normalized to that PCM at the edge. That is what makes failover safe: a chain that moves from one vendor to another mid-request cannot hand you a different format than the one it started producing.
mp3 and opus are not supported and return 400 naming what is. Encoding them
would put a codec in the audio path and make the format depend on who answered. An
earlier build accepted the value and returned PCM bytes under an mp3 label, which a
caller can neither decode nor explain — so they are refused by name instead.
Choosing the voice
Routing headers work here exactly as they do everywhere else — see routing. The two that matter most for speech:
X-Speko-Language: es-PR
X-Speko-Objective: qualityUnlike transcription, the full tag counts. es-PR and es do not rank the same: an accent tag narrows the candidate set to the vendors and voices that produce that accent, because accent narrowing applies to TTS only.
Read x-route on the response to see which provider actually served it.
Limits
- Failover is free here. Nothing has reached you yet, so a failed candidate is retried against the next one silently.
x-speko-failover-counttells you how many were tried. Streaming gives that up after its first byte. x-speko-first-byte-msis meaningful here and absent on a streaming response, where the headers are written before the audio exists.- Latency is the whole utterance. The router holds every chunk until the vendor says it is done, including for the four vendors it reaches over a WebSocket. That wait is the reason streaming exists.