VoiceMaker

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

POST/dev/v1/tts/generate

Requires the TRANSCRIPTIONS_WRITE scope.

Request parameters

ParameterTypeStatusDescription
textstringRequiredThe text to synthesize. 1–5000 characters.
voice_idstringRequiredA 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.
languagestringRequiredLanguage code: en, yo, ha, or pcm.
speednumberOptionalPlayback speed, 0.5–2.0. Default 1.0.
output_formatstringOptionalwav (default), mp3, or ogg. The returned audio_url points at a file in this format.
modestringOptionalsync (default) synthesizes inline and answers 200 with the audio. async queues the job and answers 202 — see Async mode.
cURL
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
  }'
Python
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

JSON
{
  "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:

StatuserrorCodeMeaning
503tts_warmingThe speech engine is cold-starting. Retry-After: 60 is sent — retry after it, or resubmit with mode: "async".
429concurrency_limit_reachedYour 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.

JSON
// 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:

GET/dev/v1/tts/{id}
JSON
{
  "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

GET/dev/v1/tts/voices

Returns every enabled preset voice plus your own ready custom cloned voices ("type": "custom"). Requires the TRANSCRIPTIONS_READ scope.

ParameterTypeStatusDescription
languagestringOptionalFilter voices by language code (en, yo, ha, pcm).
cURL
curl -X GET "https://api.myvoicemaker.ai/dev/v1/tts/voices?language=yo" \
  -H "Authorization: Bearer vmk_live_a1b2c3d4e5f6_8h9i0jKLmnOPqrSt"

Response — 200 Success

JSON
{
  "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

GET/dev/v1/tts/stream

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.

Code
wss://api.myvoicemaker.ai/dev/v1/tts/stream?voice_id=amaka&language=auto

The 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

ParameterTypeStatusDescription
voice_idstringRequiredA 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.
languagestringOptionalauto (default, multilingual) or ha (Hausa fine-tune). yo/ig fold into auto; en and pcm are batch-only (400 streaming_unsupported_language).
speednumberOptionalSpeaking speed 0.5–2.0, fixed for the session. Default 1.0.
qualitystringOptionalstandard (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):

  1. Authorization: Bearer vmk_... header — servers and SDKs.
  2. 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 echoes vmk.v1 back.
  3. ?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:

MessageEffect
{"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:

JSON
{"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…

JSON
{"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

CodeMeaning
1000Normal end (always preceded by session.end).
4002Purchased credits exhausted mid-stream — already-submitted audio still delivers first.
4008Idle: no text or KeepAlive for 30 seconds (synthesis in progress never counts as idle).
4009Plan limit reached: maximum stream duration or session characters.
1011Server or internal error.
1013Speech engine at capacity — retry shortly.
1012Server 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

TypeScript
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));
Python
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.