Kalpa LabsDocs
DashboardGet API key
Guides

Text-to-speech

Turn text into speech in a named voice with POST /v1/tts/{voice_id}, and tune generation with the sampling knobs.

POST /v1/tts/{voice_id} renders text as speech in a named voice. List the catalog and address a voice by id:

Shell
curl -s https://api.kalpalabs.ai/v1/voices -H "Authorization: Bearer $KALPA_API_KEY"
# {"data": [{"id": "dda5319f-…", "name": "Wren (human-like)", "gender": "feminine", "models": ["kalpa-tts-1", "kalpa-tts-beta-v0.1"]}, …]}

curl -s https://api.kalpalabs.ai/v1/tts/dda5319f-3b81-4124-b69e-9241eb510b77 \
  -H "Authorization: Bearer $KALPA_API_KEY" -H 'Content-Type: application/json' \
  -d '{
    "text": "Hey there! How are you doing today?",
    "model": "kalpa-tts-1",
    "params": {"temperature": 0.9}
  }'
200audio.data_b64Wren · kalpa-tts-1
0:02
FieldNotes
text1 – 8,000 characters; whitespace-only text is rejected with 422.
modelA public model id; omit for the default.
paramsSampling knobs, all optional — see below.
output_format"wav", the only value (the default) — see Sample rate.
sample_rateOutput sample rate in Hz (default 24000) — see Sample rate.

Each voice is a short reference clip the model continues in context — the same conversational conditioning as /v1/converse, behind a voice id — so a voice sounds the same on every request. Use converse directly when you have prior turns to condition on.

Hindi

kalpa-tts-1 (the default) speaks Hindi as well as English — send Devanagari text (naturally code-switched English words are fine), for example in the Hindi voice Kiara:

Shell
curl -s https://api.kalpalabs.ai/v1/tts/5e0c5704-590f-483b-b291-00a2415cb67e \
  -H "Authorization: Bearer $KALPA_API_KEY" -H 'Content-Type: application/json' \
  -d '{
    "text": "नमस्ते, कल्पा में आपका स्वागत है! बताइए, आज आपके लिए क्या किया जाए?"
  }'

Each model card's languages lists what it speaks — see Models.

Named voices

Not every voice is offered for every model: each voice's models lists the public model ids that speak in it, and GET /v1/voices?model=kalpa-tts-1 returns one model's voices. Asking a model for a voice it is not offered answers 404 not_found (the streaming socket closes with 4404).

For live playback, the streaming WebSocket — WSS /v1/tts/{voice_id}/stream — renders the same named voice utterance by utterance: stream text with sendText, set flush: true to hear it. Two behaviors are built for voice agents: a flush sent while a response is still generating is queued — up to 20 deep, played in flush order, each starting the moment the one ahead of it settles; a flush past that is rejected with a non-fatal error and its text discarded — and a bare cancelResponse is a barge-in — it abandons everything undelivered: the in-flight generation, every queued utterance, and any unflushed text. A cancelResponse naming a response_id cancels just that response. Every cancelResponse is answered by exactly one cancelled unless the connection closes first, in the order sent: right after the responseDone of the response it stopped, or at once when it stopped nothing. To interrupt, send a bare cancelResponse, drop audio until its cancelled arrives, then reuse the same connection for the next utterance. A cancelResponse naming a response that already finished stops nothing, and a queued utterance may already be generating. A flush with nothing to say — empty or whitespace-only text, such as an agent turn that produced no reply — is not an error: it gets a responseCreated and a completed responseDone with no audio, and nothing is billed.

When your text arrives incrementally (an LLM streaming a reply), add generation_config to initializeConnection and audio starts before the flush: once buffered text crosses the next chunk_length_schedule threshold (default [50, 80, 120, 150] characters) and ends at a complete sentence, that part starts rendering while the rest keeps arriving. The utterance stays one response on the wire — one responseCreated, one responseDone, a continuous frame counter — and its parts keep continuous prosody; flush: true still marks the end. Lower thresholds mean earlier first audio; text with no sentence boundary is cut at a clause or word gap once it runs twice the threshold, so first audio stays bounded even for unpunctuated text. If an utterance fails before the server has received its flush, you get an error with fatal: true naming its response_id, and the connection closes with code 1011: there is no responseDone, nothing is billed, and that turn's text is dropped, along with anything else you sent before the close reached you. Reconnect, then resend the turn or move on to the next one. A failure after the server has the flush keeps the connection open, as without generation_config: a non-fatal error, then responseDone with status cancelled.

Streaming over HTTP

POST /v1/tts/{voice_id}/stream renders text in a named voice and returns the audio itself while it is generated, so playback can start after the first ~80 ms instead of after the whole clip. The JSON body takes text (required) and, optionally, model, params and sample_rate. The response body is raw 16-bit little-endian PCM, mono, at sample_rate (24 kHz by default), with no header (Content-Type: audio/pcm). Play it as it arrives, or write it to a file:

Shell
curl -fsSN https://api.kalpalabs.ai/v1/tts/dda5319f-3b81-4124-b69e-9241eb510b77/stream \
  -H "Authorization: Bearer $KALPA_API_KEY" -H 'Content-Type: application/json' \
  -d '{"text": "Hey there! How are you doing today?"}' \
  | ffplay -f s16le -ar 24000 -nodisp -autoexit -
200audio.data_b64Wren · kalpa-tts-1
0:02

Each request is one utterance, which suits voice-agent frameworks that synthesize a reply sentence by sentence over HTTP. To stream text into one utterance as it arrives, use the streaming WebSocket.

The response starts with the first audio, so a request that fails before any audio gets an ordinary error response: its status code and the error envelope, including 503 overloaded with Retry-After. If generation fails after audio has started, the connection is terminated before the response completes, so a cut-off clip never looks complete. Closing the connection stops generation. Only completed streams are billed.

The audio you get back

JSON
{
  "request_id": "…",
  "model": "kalpa-tts-1",
  "text": "Hey there! How are you doing today?",
  "audio": {
    "format": "wav",
    "sample_rate": 24000,
    "audio_quality": "high",
    "data_b64": "UklGRi…"
  },
  "usage": { "input_chars": 35, "input_audio_seconds": 0.0, "output_audio_seconds": 2.6 },
  "meta": {}
}

request_id matches the X-Request-ID response header and exists to correlate this call with client and server logs; see Request IDs.

data_b64 is a complete 16-bit PCM WAV file (mono, at sample_rate), base64-encoded — decode it and play:

Python
import base64
with open("out.wav", "wb") as f:
    f.write(base64.b64decode(reply["audio"]["data_b64"]))

audio_quality echoes the fidelity tier the waveform was rendered at (see the audio_quality knob).

Sample rate

Set sample_rate to receive audio at the rate your pipeline runs at — for example 8 kHz for telephony or 48 kHz for WebRTC:

FieldValuesDefault
output_format"wav" on POST /v1/tts/{voice_id}; "pcm_s16le" (raw 16-bit little-endian PCM) on POST /v1/tts/{voice_id}/stream and the streaming WebSocketThe endpoint's only value
sample_rate8000, 16000, 22050, 24000, 32000, 44100, 4800024000

Send both in the request body, or in initializeConnection for the WebSocket, where they hold for the whole connection. Output is always mono 16-bit. The rate in use is echoed back: audio.sample_rate in the REST response, sample_rate in sessionCreated and every responseCreated on the WebSocket; the HTTP stream's body is at the rate you sent. Any other value is rejected — 422 on REST; on the WebSocket, an invalid_request error followed by close 4400. Usage is billed by audio duration, which the rate doesn't change.

Generation parameters

All fields of params are optional; the defaults are the tuned starting point. The same knobs (with the live defaults and UI ranges) are served at GET /v1/info.

ParamDefaultRangeWhat it does
temperature0.90 – 1.5Sampling temperature for the words and delivery. 0 = deterministic.
acoustic_temperaturenull0 – 1.5Temperature for the voice's fine acoustic detail. null = follows temperature.
audio_quality"high"low / medium / highPlayback fidelity. high is full quality; lower tiers reduce fidelity — not payload size (the WAV is always full-duration 16-bit PCM).

One practical note: for reproducible output, set temperature to 0 — with greedy decoding the same request returns the same speech.

If generation reaches the model's context limit before a stop token, the API returns context_length_exceeded; try shorter sentences.