API reference
Every endpoint of the Yoob voice API. All of them live under https://tts-api.yoob.com/v1 and take an API key as Authorization: Bearer …, except health.
Create speech
/v1/audio/speechTurns text into audio. OpenAI-compatible, with Yoob's options added. See the text to speech guide.
Body (JSON)
inputstringrequired- The text to speak, up to 4,096 characters, with any tags.
voicestringrequired- A voice id from List voices, such as
w1243.{"id": "w1243"}is accepted too. modelstringyoob-tts-1, the default. Echoed back inx-yoob-model.qualitystringbest(default),best_allorfast. See Quality.response_formatstringwav(default),pcm,mp3,opus,aacorflac. Always 48 kHz mono.streamboolean- Send the audio chunk by chunk as it is rendered. Default
false. speednumber- 0.5 to 2.0, default 1.0.
instructionsstring- Free-text delivery, up to 300 characters.
seedinteger | string- Makes the render reproducible. A string of 1–64 characters of
[A-Za-z0-9_.:-]works as well. pickstringdefaultorexpressive: howbestranks its takes.request_idstring- Your id for the request, 1–64 characters of
[A-Za-z0-9._-], so it can be cancelled.
curl https://tts-api.yoob.com/v1/audio/speech \
-H "Authorization: Bearer $YOOB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "yoob-tts-1", "voice": "w1243", "input": "[joy] Hello!", "quality": "fast"}' \
--output hello.wavResponse
The audio, with these headers:
content-type: audio/wav
x-request-id: req_b731ca0a249f49e69c89
x-yoob-seed: 1778055142
x-yoob-quality: fast
x-yoob-sample-rate: 48000
x-yoob-audio-seconds: 1.04
x-yoob-chunks: 1x-yoob-audio-seconds is sent when the whole file is returned at once, not with stream.
Stream speech (WebSocket)
/v1/audio/speech/streamVoices text that is still being written, one connection per reply. Authenticate with the Authorization header, or put api_key in the config message. See Streaming text in.
| You send | When |
|---|---|
{"voice", "quality", "response_format", ...} | First: the config, the same fields as Create speech except input, plus min_words (default 3) and normalize (default true). response_format is pcm (default), mp3 or opus. |
{"text": "..."} | Text as it is written |
{"flush": true} | Speak what is buffered now |
{"end": true} | The end of the text; may also carry text and a flush |
| You receive | When |
|---|---|
{"type": "ready", "request_id", "sample_rate": 48000, "seed", ...} | After the config |
{"type": "chunk", "index", "text", "audio_seconds"} | Before each chunk's audio, which follows as binary frames |
{"type": "done", "chunks", "audio_seconds", "picks"} | All audio sent; the socket closes with 1000 |
{"type": "error", "error": {"message", "code"}} | Then the socket closes |
The binary frames of all chunks together form one continuous stream. 30 s with no message counts as the end; at most 20,000 characters per connection; closing the socket cancels the reply.
| Error code | Close | When |
|---|---|---|
invalid_api_key | 1008 | Missing or wrong key |
bad_config | 1008 | The config is invalid |
bad_message | 1008 | An invalid message, or over the character cap |
engine_loading | 1013 | The engine is starting; retry shortly |
busy | 1013 | Too many streams are open |
synthesis_failed | 1011 | Rendering failed |
Cancel speech
/v1/audio/speech/cancelStops a running speech request by the request_id you gave it, as a disconnect would but at once. Idempotent. A request cancelled before it starts streaming answers 409 request_cancelled.
Body (JSON)
request_idstring- One id to cancel.
request_idsstring[]- Or up to 64 of them.
{"cancelled": true} // running, and now cancelled
{"cancelled": false} // no running request has that id
{"cancelled": {"reply-41": false, "reply-42": true}} // for request_idsList voices
/v1/voices{
"object": "list",
"data": [
{"id": "w1243", "object": "voice", "gender": "female", "tier": "product", "description": "Woman, mid twenties, American, mid-range pitch; breathy, intimate, hushed. ..."},
...
]
}List models
/v1/modelsAlso GET /v1/models/yoob-tts-1 for one model.
{"object": "list", "data": [
{"id": "yoob-tts-1", "object": "model", "created": 1760745600, "owned_by": "yoob"},
{"id": "yoob-asr-1", "object": "model", "created": 1760745600, "owned_by": "yoob"}
]}Create transcription
/v1/audio/transcriptionsTranscribes audio in any common format, at most 25 MB: up to 60 minutes with yoob-asr-2, up to 60 seconds with yoob-asr-1. See the speech to text guide and emotion recognition.
Multipart form (as the OpenAI SDKs send it)
fileformrequired- The audio file.
modelformyoob-asr-2(most accurate, text only) oryoob-asr-1(the default; adds emotion, tags and the voice profile inverbose_json).response_formatformjson(default),textorverbose_json.
Or a raw body
Content-Typeheaderrequired- The audio's type, such as
audio/wavoraudio/mpeg. The body is the file. modelquery- As above.
response_formatquery- As above.
curl "https://tts-api.yoob.com/v1/audio/transcriptions?response_format=json" \
-H "Authorization: Bearer $YOOB_API_KEY" \
-H "Content-Type: audio/wav" \
--data-binary @clip.wavResponse
{"text": "We still got some work to do and I look forward to getting that work done."}verbose_json adds caption, segments (with caption, tags, emotion, emotion_probs, words, voice and p_human), words, duration and speaker. A full example is in the guide.
Stream transcription (WebSocket)
/v1/audio/transcriptions/streamLive speech to text, up to 60 s of audio per session. Authenticate with the Authorization header, or send {"type": "auth", "api_key": "..."} as the first message. See Realtime.
wss://tts-api.yoob.com/v1/audio/transcriptions/stream| You send | What |
|---|---|
| Binary frames | Raw PCM: 16 kHz, signed 16-bit little-endian, mono. Even length, at most 1 s (32,000 bytes) each |
{"type": "ping", "t": <number>} | Answered with {"type": "pong", "t", "stream_t"} |
{"type": "end"} | End the current utterance now; the session stays open |
{"type": "stop"} | Finish: the last utterance is finalised, then done, then the socket closes |
Anything else ends the session with an error (bad_message, or bad_frame for audio of the wrong shape). Events sent back are listed in Realtime events.
{"type": "ready", "id": "34f8d689b6b7", "sample_rate": 16000, "max_seconds": 60.0, "partial_every": 0.3}
{"type": "vad", "speech": true, "t": 0.064, "utt": 1}
{"type": "partial", "utt": 1, "start": 0.0, "end": 0.32, "caption": "[emo:neutral] Hello?", "text": "Hello?", "emotion": "neutral", ...}
{"type": "final", "utt": 1, "reason": "silence", "caption": "[emo:joy] Hello there. I am so happy you came today.", ...}
{"type": "final_meta", "utt": 1, "voice": {"f0_hz": 237.9, "wpm": 234.9, "snr_db": 56.9}, "p_human": 0.0002}
{"type": "speaker", "seconds": 4.5, "gender": {"m": 0.0069, "f": 0.9931, "o": 0}, "accent": {"us": 0.9953, ...}, ...}
{"type": "done"}Duotone call (WebSocket)
/v1/duotoneA live voice call you can interrupt, one connection per call. Authenticate with the Authorization header, or put api_key in the config. See Use the Duotone socket.
| You send | When |
|---|---|
{"voice", "quality", "greeting", "instructions", "history", "llm"} | First, within 10 s: the config. Every field is optional; see the config fields. |
| binary frames | The caller's microphone: s16le mono 16 kHz, at most 1 s each, for the whole call |
{"type": "text", "text": "..."} | A typed turn; cuts in like speech |
{"type": "stop"} | Hang up |
| You receive | When |
|---|---|
{"type": "ready", "session_id", "sample_rate": 48000, "input_sample_rate": 16000, "max_seconds": 600} | After the config |
{"type": "user", "final", "caption", "text", "emotion"} | What the caller is saying, live; final at the end of each turn |
{"type": "reply", "id", "delta"} | The reply as it is written, voice tags included |
| binary frames | The reply's audio: s16le mono 48 kHz |
{"type": "reply_done", "id", "text", "interrupted"} | The reply is complete, or was cut off by the caller |
{"type": "error", "code", "message"} | A failed turn (the call goes on), or a refused call (the socket then closes) |
{"type": "done", "usage"} | The call has ended; the socket closes with 1000 |
{"type": "done", "usage": {"listened_seconds": 41.2, "spoken_characters": 612,
"llm_input_tokens": 5120, "llm_output_tokens": 186, "llm": "yoob"}}llm is yoob when Yoob's model wrote the replies, and yours when the config brought one; its tokens are then not billed. Calls end after 600 s.
| Error code | Close | When |
|---|---|---|
bad_config | 1008 | No config within 10 s, or it is not a JSON object |
invalid_api_key | 1008 | Missing or wrong key |
insufficient_credits | 1008 | The key's workspace is out of credits |
engine_loading | 1013 | The voice engine is starting; retry shortly |
busy | 1013 | Every live session is in use; retry shortly |
not_configured | 1011 | No language model is available; bring your own in llm |
bad_frame | – | An audio frame of an odd length or over 1 s; the call goes on |
reply_failed | – | One reply failed; the call goes on |
internal_error | 1011 | The call failed |
Health
/healthNo key needed, and not under /v1: https://tts-api.yoob.com/health. Answers 200 when the service is ready and 503 otherwise.
{"status": "ok", "model": "yoob-tts-1", "queue": {"running": 0, "waiting": 0, "max_waiting": 16, "streams": 0, "max_streams": 16}}Errors
Errors use OpenAI's shape. param names the field at fault, when there is one.
{"error": {"message": "'input' is required and must be a non-empty string.", "type": "invalid_request_error", "param": "input", "code": null}}| Status | code | When |
|---|---|---|
| 400 | null, input_too_long, invalid_voice | A missing or bad field; see param |
| 401 | missing_api_key, invalid_api_key | No key, or one that is wrong or revoked |
| 404 | not_found, model_not_found | Unknown endpoint or model |
| 409 | request_id_in_use, request_cancelled | A request_id already running, or cancelled |
| 429 | rate_limited, queue_full, busy | Too many requests, sessions or waiting renders; retry after Retry-After |
| 500 | synthesis_failed, internal_error | The engine failed. A stream that fails part-way just ends early |
| 503 | engine_loading | Right after a restart, for about 30 s |
Rate limits
| Limit | Value |
|---|---|
| Requests | 300 a minute per key and per IP address, with bursts of up to 50 over that |
| Sockets | Each WebSocket counts once, however much is sent over it |
| Realtime transcription | Shared capacity; a session over it is refused with busy |
A 429 carries a Retry-After header in seconds; wait that long before retrying. For conversations, prefer the speech socket over one request per sentence: it counts once per reply.