POST /v1/audio/speech returns the audio file of a text spoken in a voice. The request and response shapes are
OpenAI's. The key needs the media:generate scope.
Parameters
A JSON body. A field outside the list fails with 400 bad-param. A parameter the model has no use for is dropped,
as on OpenAI.
| Name | Type | Description |
|---|---|---|
input required | string | The text to speak. At most 5,000 characters; 20,000 on kokoro |
voice required | string | A built-in voice of the model, or a voice reference (table below) |
model | string | A speech model id, such as kokoro, omnivoice or qwen3-tts, or OpenAI's tts-1 or tts-1-hd. Required unless voice is a saved voice vo_…, whose default_model then runs |
response_format | string | mp3 (default), wav or opus |
speed | number | 0.5 to 2.0, default 1.0. Used by kokoro and omnivoice |
instructions | string | A description of the delivery, passed to models with an instruct parameter (breeze2; omnivoice ignores the field when voice is given) |
stream_format | string | audio only: the whole file in one response |
voice form | Value |
|---|---|
| Built-in name | A voice of the model, such as af_heart on kokoro; OpenAI's names (alloy, nova, ...) map to Kokoro voices |
vo_… | A saved voice of the account, or a shared voice |
up_… | An upload of the account (POST /v1/media/uploads) |
job:<id>/<i> | Result i of a finished job of the account |
https://… | A clip fetched when the request arrives, 20 MB at most, from public hosts only |
model | Runs as |
|---|---|
tts-1 | kokoro; omnivoice when voice is a voice reference |
tts-1-hd | kokoro; qwen3-tts when voice is a voice reference |
| Left out | The saved voice's default_model |
Speech models and each model's parameters: GET /v1/media/models?kind=speech
(Media jobs reference).
Headers
| Name | Description |
|---|---|
Idempotency-Key | Request. A request with the same key and body returns the same job's file. Without the header, an identical request in the same 10-minute clock window returns the first request's job; a job that failed, expired or was cancelled runs again |
X-Queue-Timeout | Request. Seconds the job may wait in the queue, a whole number of 1 or more; a value above the kind's queue timeout is lowered to the queue timeout |
Content-Type | Response. audio/mpeg, audio/wav or audio/ogg |
X-Job-Id | Response. The media job mj_… that made the file |
X-Queue-Timeout | Response. The job's queue timeout in seconds |
X-Duration-S | Response. The length of the audio in seconds |
Retry-After | Response, on 503. 5 |
X-Should-Retry | Response, on 503 queued and 503 queue-timeout. false: OpenAI SDKs do not repeat the request |
Request
curl
curl https://api.albastov.com/v1/audio/speech \
-H "Authorization: Bearer $ALBASTOV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "kokoro", "voice": "af_heart", "input": "The river bends past the old mill.", "response_format": "mp3"}' \
--dump-header - --output river.mp3Python
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.albastov.com/v1", api_key=os.environ["ALBASTOV_API_KEY"])
with client.audio.speech.with_streaming_response.create(
model="kokoro", voice="af_heart", input="The river bends past the old mill.", response_format="mp3"
) as res:
print(res.headers["x-job-id"], res.headers["x-duration-s"])
res.stream_to_file("river.mp3")JavaScript
import { writeFile } from "node:fs/promises";
const res = await fetch("https://api.albastov.com/v1/audio/speech", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALBASTOV_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "kokoro", voice: "af_heart", input: "The river bends past the old mill.", response_format: "mp3",
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(res.headers.get("x-job-id"), res.headers.get("x-duration-s"));
await writeFile("river.mp3", Buffer.from(await res.arrayBuffer()));Response
200 with the audio file as the body.
HTTP/2 200
content-type: audio/mpeg
x-job-id: mj_HSRJJxSiza6v7xZULYpwgh
x-queue-timeout: 300
x-duration-s: 2.360The request waits for its job up to the job's queue timeout while the job is queued, and at most 85 s from arrival.
An account holds 16 waiting requests at most; past that, the route answers 503 queued at once. The job stays in
GET /v1/media/jobs/{id} after a 503.
Errors
Errors use OpenAI's shape, {"error": {"message", "type", "param", "code"}}, plus job_id when a job exists.
Branch on code.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad-param | A field is missing, unknown or out of range; param names the field (input, voice, model, response_format, stream_format, speed, X-Queue-Timeout) |
| 400 | bad-json | The body is not a JSON object |
| 404 | voice_not_found | No saved voice with the id, or a voice the key may not use |
| 409 | voice_not_ready | A designed voice still pending, or failed |
| 413 | too-large | The body is over 4 MB |
| 422 | voice_not_supported | The model does not speak saved voices; models lists the models that do |
| 429 | budget-exhausted | The account's daily media time is used up; resets_at is the next 00:00 UTC |
| 429 | too-many-open | The key has 1,000 jobs queued or running |
| 502 | The job's error code | The job failed or was cancelled; job_id names the job |
| 503 | queued | The job is still queued; job_id names the job |
| 503 | queue-timeout | The job waited past its queue timeout and expired |
| 503 | voice_model_unavailable | No model that speaks saved voices is online for a request without model |
Key, scope and rate-limit errors: Errors.