Text-to-Speech (TTS)
Generate natural, dialect-accurate speech in English, Yoruba, Hausa, and Nigerian Pidgin across multiple voice personas and styles. TTS is synchronous — the response contains a ready-to-use audio URL. For audio that starts playing within seconds while longer text is still generating, use real-time streaming.
Supported output formats
MP3, WAV, OGG.
Voices
VoiceMaker offers a curated set of male and female voice personas, each with one or
more delivery styles (e.g. conversational, narrative). Fetch the current catalogue
at runtime with GET /dev/v1/tts/voices and use a
returned id as your voice_id. The listing also includes your own custom
cloned voices (created with Clone For Me in the web app) — marked with
"type": "custom" — and their ids work everywhere a preset id does: batch
generation and real-time streaming.
Generate speech
Requires the TRANSCRIPTIONS_WRITE scope.
Request parameters
| Parameter | Type | Status | Description |
|---|---|---|---|
text | string | Required | The text to synthesize. 1–5000 characters. |
voice_id | string | Required | A voice ID from GET /dev/v1/tts/voices — a preset ID or the ID of one of your own custom cloned voices. Omitting it answers 400 with voice_id_required. A custom voice that exists but cannot synthesize yet (still processing or failed) answers 422 with voice_not_ready. |
language | string | Required | Language code: en, yo, ha, or pcm. |
speed | number | Optional | Playback speed, 0.5–2.0. Default 1.0. |
output_format | string | Optional | wav (default), mp3, or ogg. The returned audio_url points at a file in this format. |
mode | string | Optional | sync (default) synthesizes inline and answers 200 with the audio. async queues the job and answers 202 — see Async mode. |
curl -X POST https://api.myvoicemaker.ai/dev/v1/tts/generate \
-H "Authorization: Bearer vmk_live_a1b2c3d4e5f6_8h9i0jKLmnOPqrSt" \
-H "Content-Type: application/json" \
-d '{
"text": "Ẹ kú àárọ̀, ẹ káàbọ̀ sí VoiceMaker.",
"voice_id": "amara-okafor-female-conversational",
"language": "yo",
"speed": 1.0
}'from myvoicemaker import VoiceMaker
client = VoiceMaker(api_key="vmk_live_a1b2c3d4e5f6_8h9i0jKLmnOPqrSt")
result = client.tts.generate(
text="Ẹ kú àárọ̀, ẹ káàbọ̀ sí VoiceMaker.",
voice_id="amara-okafor-female-conversational",
language="yo",
speed=1.0,
)
print(result.audio_url)
print(result.credits_used)Response — 200 Success
{
"requestId": "8f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"id": "vit_1a2b3c4d5e6f7g8h9i0j",
"audio_url": "https://media.myvoicemaker.ai/voice-it-generated/.../tts-api.wav",
"duration_seconds": 3.2,
"characters": 31,
"credits_used": 31,
"language": "yo",
"voice_id": "amara-okafor-female-conversational",
"output_format": "wav",
"created_at": "2026-03-03T10:30:00Z"
}Errors
Sync mode holds the HTTP connection while the engine synthesizes, so two retryable errors are specific to it:
| Status | errorCode | Meaning |
|---|---|---|
503 | tts_warming | The speech engine is cold-starting. Retry-After: 60 is sent — retry after it, or resubmit with mode: "async". |
429 | concurrency_limit_reached | Your plan's processing-job cap is in use. Retry-After: 30 is sent — retry after it, or resubmit with mode: "async". |
Other errors (voice_id_required, unknown_voice_id, voice_not_ready,
unsupported_language, insufficient_credits, synthesis_failed, …) are
listed on the Error Codes page.
Async mode
Pass mode: "async" to queue the job instead. It never hits the concurrency
cap and survives engine cold starts; credits are reserved up front and settled
to the real cost on completion.
// 202 Accepted
{
"requestId": "8f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"id": "vit_1a2b3c4d5e6f7g8h9i0j",
"status": "queued",
"characters": 31,
"credits_reserved": 31,
"language": "yo",
"voice_id": "amara-okafor-female-conversational",
"output_format": "wav",
"created_at": "2026-03-03T10:30:00Z"
}Poll the job until status is completed or failed:
{
"requestId": "…",
"id": "vit_1a2b3c4d5e6f7g8h9i0j",
"status": "completed",
"audio_url": "https://media.myvoicemaker.ai/voice-it-generated/.../tts-api.wav",
"duration_seconds": 3.2,
"characters": 31,
"credits_used": 31,
"language": "yo",
"voice_id": "amara-okafor-female-conversational",
"output_format": "wav",
"error": null,
"created_at": "2026-03-03T10:30:00Z",
"completed_at": "2026-03-03T10:30:04Z"
}audio_url is null until the job completes; error carries the failure
message when status is failed. An unknown or foreign id answers 404
with not_found. The SDKs wrap this as tts.generate({ mode: "async" }) /
tts.generate(..., mode="async") plus tts.getResult() / tts.get_result()
and tts.poll().
List available voices
Returns every enabled preset voice plus your own ready custom cloned voices
("type": "custom"). Requires the TRANSCRIPTIONS_READ scope.
| Parameter | Type | Status | Description |
|---|---|---|---|
language | string | Optional | Filter voices by language code (en, yo, ha, pcm). |
curl -X GET "https://api.myvoicemaker.ai/dev/v1/tts/voices?language=yo" \
-H "Authorization: Bearer vmk_live_a1b2c3d4e5f6_8h9i0jKLmnOPqrSt"Response — 200 Success
{
"voices": [
{
"id": "amara-okafor-female-conversational",
"name": "Amara Okafor",
"gender": "female",
"language": "yo",
"style": "conversational",
"sample_url": "https://media.myvoicemaker.ai/voices/amara-okafor-conversational.wav",
"type": "preset"
},
{
"id": "cmxyz123abc456def789ghi0",
"name": "My Studio Clone",
"gender": "male",
"language": "English",
"style": "Personal",
"sample_url": "https://media.myvoicemaker.ai/custom-voices/.../reference.wav",
"type": "custom"
}
]
}Real-time streaming
Stream text over a WebSocket (wss://) and receive audio chunk-by-chunk while
later sentences are still synthesizing — first audio typically lands within a
few seconds on a warm engine. Requires the TRANSCRIPTIONS_WRITE scope.
wss://api.myvoicemaker.ai/dev/v1/tts/stream?voice_id=amaka&language=autoThe server splits your text into sentence-sized chunks and synthesizes them in
order; each chunk arrives as chunk.start → binary PCM frames → chunk.end.
Output is fixed: raw PCM, 16-bit little-endian, mono, 24 000 Hz
(pcm_s16le) — the SDKs include helpers to play it in a browser or wrap it in
a WAV container.
Connection parameters
| Parameter | Type | Status | Description |
|---|---|---|---|
voice_id | string | Required | A preset ID from GET /dev/v1/tts/voices — or the ID of one of your own custom cloned voices. Omitting it answers 400 with voice_id_required before the upgrade. |
language | string | Optional | auto (default, multilingual) or ha (Hausa fine-tune). yo/ig fold into auto; en and pcm are batch-only (400 streaming_unsupported_language). |
speed | number | Optional | Speaking speed 0.5–2.0, fixed for the session. Default 1.0. |
quality | string | Optional | standard (default; lowest latency) or high (more diffusion steps per chunk, roughly double the chunk latency). |
Authentication
Pick whichever channel your client supports (checked in this order):
Authorization: Bearer vmk_...header — servers and SDKs.Sec-WebSocket-Protocol: vmk.v1, vmk.key.vmk_...— recommended for browsers, which cannot set headers on WebSockets; keeps the key out of URLs and access logs. The server echoesvmk.v1back.?api_key=vmk_...query parameter — last resort; the key will appear in server access logs.
Auth, voice, plan and credit checks happen before the upgrade, so failures
are plain HTTP responses (400 with voice_id_required, 401, 402, 403,
422 with unknown_voice_id or voice_not_ready,
429 with stream_concurrency_limit, or 503). A cold speech engine answers
503 with errorCode: "tts_warming" and Retry-After: 60 — retry the
connection in about a minute rather than holding a socket open.
Sending text
All client messages are JSON text frames:
| Message | Effect |
|---|---|
{"type": "Text", "text": "...", "id": "optional-tag"} | Submit text — a whole paragraph or incremental sentences (type-ahead / LLM output). Max 2000 characters per frame. The server holds back a trailing incomplete sentence until more text arrives; a frame boundary is treated as a word boundary. id (≤64 chars) is echoed back on the chunks this text starts. |
{"type": "KeepAlive"} | Resets the 30s idle timer without sending text. |
{"type": "Finalize"} | Synthesizes the held-back tail; the stream stays open. Acked with {"type": "finalized"} once everything submitted has been delivered. |
{"type": "CloseStream"} | Synthesizes and delivers everything remaining, emits session.end, then closes with code 1000. |
Cumulative text per session is capped by your plan's max text characters
(1 000 on Free, 5 000 on paid plans); exceeding it raises
session_char_limit — audio already submitted still delivers before the
session closes with code 4009.
Receiving audio
Audio arrives as binary frames (≤ ~1 s of PCM each) between JSON chunk markers:
{"type": "session.begin", "session_id": "tts_stream_...", "request_id": "...",
"language": "auto", "voice_id": "amaka", "sample_rate": 24000,
"encoding": "pcm_s16le", "quality": "standard", "speed": 1.0,
"credits_per_character": 1, "max_session_characters": 5000,
"max_duration_seconds": 1800}
{"type": "chunk.start", "index": 0, "submission_id": null,
"text": "Sannu da zuwa.", "characters": 14, "credits": 14}…binary PCM16LE frames for chunk 0…
{"type": "chunk.end", "index": 0, "submission_id": null, "characters": 14,
"credits": 14, "duration_seconds": 1.62, "audio_bytes": 77760}
{"type": "session.end", "session_id": "tts_stream_...", "total_characters": 512,
"credits_used": 512, "duration_seconds": 41.7, "chunks": 4,
"reason": "client_close"}session.end is always sent before the socket closes; reason is one of
client_close, idle_timeout, max_duration, max_characters,
insufficient_credits, error, server_shutdown. Errors surface as
{"type": "error", "code": "...", "message": "..."} followed by a close.
Close codes
| Code | Meaning |
|---|---|
1000 | Normal end (always preceded by session.end). |
4002 | Purchased credits exhausted mid-stream — already-submitted audio still delivers first. |
4008 | Idle: no text or KeepAlive for 30 seconds (synthesis in progress never counts as idle). |
4009 | Plan limit reached: maximum stream duration or session characters. |
1011 | Server or internal error. |
1013 | Speech engine at capacity — retry shortly. |
1012 | Server restarting — reconnect. |
Billing & limits
Streaming bills purchased credits only, at the same per-character rate as batch TTS (1 credit per character by default), metered on synthesized characters. Credits are reserved in blocks as you submit text and settled to the exact synthesized total when the session ends — unused reserve is refunded immediately, and a session that synthesized nothing is fully refunded. Each connection also counts as one request against your per-minute rate limit.
Concurrent streams and the maximum session duration follow the same per-plan
limits as ASR streaming. When the session ends, GET /dev/v1/tts/{id} (with
the session's id) returns its totals — the streamed audio itself is not
stored; capture it client-side (the SDKs include a WAV helper).
SDK example
import { VoiceMaker, createPcmPlayer } from 'myvoicemaker';
const client = new VoiceMaker({ apiKey: 'vmk_...' });
const stream = client.tts.stream({ voiceId: 'amaka', language: 'auto' });
const player = createPcmPlayer(); // browser: Web Audio
stream.on('audio', ({ data }) => player.enqueue(data));
stream.on('chunkEnd', (c) => console.log(`chunk ${c.index}: ${c.duration_seconds}s`));
stream.sendText('Barka da zuwa. Yaya kake a yau?');
const summary = await stream.close(); // drains, then session.end
// Node instead of a browser: collect + save
// const chunks = []; for await (const pcm of stream.audioChunks()) chunks.push(pcm);
// await fs.writeFile('out.wav', pcmChunksToWav(chunks));from myvoicemaker import VoiceMaker
client = VoiceMaker(api_key="vmk_...")
with client.tts.stream(voice_id="amaka", language="auto") as conn:
conn.send_text("Barka da zuwa. Yaya kake a yau?")
end = conn.stream_to_file("out.wav") # writes chunks as they arrive
print(end.chunks, end.credits_used)
# Live speaker playback: iterate conn.audio_chunks(close=True) and feed a
# sounddevice OutputStream — see the TtsStreamConnection docstring.