Conversations
POST /v1/converse completes the open turn of a conversation — authored speech, contextual TTS, and spoken history.
POST /v1/converse is the API's core. You send one conversation — a list of turns, oldest first — and the model completes its last turn. There is no separate "target text" or "target speaker" field; the last turn is the request.
A turn has three fields, all combining naturally:
{ "speaker": "0", "text": "…", "audio_wav_b64": "…" }The rules:
- Every turn except the last is grounded history: it must carry
textand/oraudio_wav_b64. - The last turn is the open turn — the one to complete.
speakeralone means the model authors the turn;speaker+textmeans it renders exactly that text. An emptytext("") counts as omitted — it flips the request to authoring, so don't send the field unless it has content. audio_wav_b64is never used on the open turn: together withtextit's a400(nothing left to generate); alone it is discarded unread — not decoded, not billed — and the turn is authored as if it were speaker-only. Audio belongs on history turns.
Author the next turn
An open turn with only a speaker asks the model to write and voice it:
curl -s https://api.kalpalabs.ai/v1/converse \
-H "Authorization: Bearer $KALPA_API_KEY" -H 'Content-Type: application/json' \
-d '{
"conversation": [
{"speaker": "0", "text": "Did you end up trying that recipe?"},
{"speaker": "1", "text": "I did — the timing was the hard part."},
{"speaker": "0"}
]
}'The reply carries the authored text and its audio:
{
"request_id": "…",
"model": "kalpa-tts-beta-v0.1",
"reply": {
"speaker": "0",
"text": "Same here. Did you keep the flame low like she said?",
"audio": { "format": "wav", "sample_rate": 24000, "audio_quality": "high", "data_b64": "…" }
},
"usage": { "input_chars": 71, "input_audio_seconds": 0.0, "output_audio_seconds": 2.4 },
"meta": { "mode": "converse_author", "generate_ms": 812.4, "frames": 30, "duration_sec": 2.4, "context_turns": 2 }
}Contextual TTS: render exact text, in context
An open turn with speaker and text renders exactly that text, conditioned on everything before it — same voice, continued rhythm. This is how you re-voice an edited turn or drive a scripted dialogue:
{
"conversation": [
{"speaker": "0", "text": "Welcome back to the show.", "audio_wav_b64": "<reply.audio.data_b64 from an earlier call>"},
{"speaker": "1", "text": "Glad to be here."},
{"speaker": "0", "text": "Let's pick up where we left off — episode twelve."}
]
}The first turn's audio_wav_b64 is a complete WAV from an earlier reply.audio.data_b64. It establishes speaker "0"'s voice; the model then speaks the final, text-only open turn in that voice. Text-only history still supplies linguistic context, but it cannot by itself identify a particular voice.
Spoken history and reference audio
Any history turn may carry audio_wav_b64 — base64 16-bit PCM WAV, up to 25 MiB decoded per turn. Any sample rate is accepted and multi-channel audio is mixed down to mono; a data: URI prefix is tolerated. Two uses:
- Spoken history: pass the actual audio of earlier turns (yours or previous API replies) so the model hears the conversation instead of just reading it.
- Reference voice: open a conversation with a turn containing a short clip of a voice, then have that
speakercomplete the open turn — the model continues in that voice.
usage.input_audio_seconds meters the audio you send; see Usage.
Speaker labels
speaker values are positional role labels, not names — the labels the model was trained on, listed per model in GET /v1/models (the current conversational models use "0" and "1", in turn order). Two things matter:
- Keep a label bound to one voice within a conversation:
"0"is whoever spoke first,"1"the other party. - An omitted
speakerdefaults to"0"— on every turn. Fine for single-speaker requests; in a dialogue, set it explicitly or all turns silently collapse into one speaker.
Streaming the reply
POST /v1/converse/stream takes exactly the same request body as /v1/converse and streams the reply as server-sent events instead of one JSON response, so playback can start after the first ~80 ms audio frame. The stream is one meta event, audio events as audio is generated, then exactly one end — or one error:
event: meta
data: {"model":"kalpa-tts-beta-v0.1","mode":"converse_author","sample_rate":24000,"audio_quality":"high","context_turns":2}
event: audio
data: {"pcm_b64":"…","frame":1}
event: audio
data: {"pcm_b64":"…","frame":3}
event: end
data: {"request_id":"…","text":"Same here. Did you keep the flame low?","sample_rate":24000,"audio_quality":"high","audio_seconds":2.4,"usage":{"input_chars":71,"input_audio_seconds":0.0,"output_audio_seconds":2.4},"meta":{"mode":"converse_author","generate_ms":812.4,"frames":30,"duration_sec":2.4,"context_turns":2}}- Each audio event's
pcm_b64is base64 raw 16-bit little-endian PCM, mono, atmeta.sample_rate— no WAV header (the same chunks the WebSocket emits). Decode each chunk and append: the concatenation, in order, is the full waveform.frameis the cumulative model-frame count (12.5 frames = 1 s). endcarries the reply text (authored text is only known at the end), the finalusage, and diagnosticmeta. There is nodata_b64— the audio already streamed past you.- A request that fails before any generation — validation, auth, an unreachable backend — is a plain JSON error response in the standard envelope, not an event stream. A failure mid-stream ends the stream with an
errorevent:{"error": "…"}. A stream that ends with neitherendnorerrorwas cut by the network — treat it as failed. - Closing the connection aborts generation. Only streams that reach
endare metered; an aborted stream bills nothing.
Limits
| Cap | Value | Error when exceeded |
|---|---|---|
| Turns per conversation | 64 | 422 invalid_request |
| Text per turn | 8,000 characters | 422 invalid_request |
| Audio per turn | 25 MiB decoded WAV | 400 invalid_request |
Schema-bound turn and text limits fail before generation; audio decoding and semantic turn-rule failures return 400 invalid_request. See Rate limits & errors.
For live turns, use the stateful WebSocket. It keeps the
conversation history for the connection and streams audio as it is generated. Put the same
generation params used by REST/SSE in initializeConnection; they apply to every response on the
socket. Set optional history_limit to retain only that many completed turns when a response
starts. responseDone carries the final usage and public timing metadata.