POST /v1/tts POST¶
POSThttps://api.vartalaap.si/v1/tts
Synthesise text with voice and stream the audio back in format. The response starts after the first 80 ms frame is decoded.
POST /v1/tts HTTP/1.1
Host: api.vartalaap.si
x-api-key: vt_live_…
content-type: application/json
{"text": "Callback +44 00000 19024 पर करें।", "voice": "saanvi", "format": "wav"}
Request body¶
| Field | Type | Default | Notes |
|---|---|---|---|
text |
string | required | ≤ 4 000 characters after trimming. Run through the Hinglish normaliser unless raw is true. Must leave something pronounceable (else 400 nothing to say after normalisation). Splits into ≤ 64 sentence chunks of about 50 tokens (a clause with no sentence or comma boundary inside it stays whole and may exceed that; words are never dropped to fit). |
voice |
string | saanvi |
Any id from GET /v1/voices — the twenty-one Hinglish voices (saanvi, sahil, meera, … mahesh) the eighteen English (US) ones (kiara, kevin, …) or the eight English (IN) ones (ananya, arjun, …). Unknown → 400; not allowed for the key → 403. The voice decides the language and so the normaliser: English text to an English (US) or English (IN) voice, Hinglish text to a Hinglish one. |
format |
string | wav |
wav · pcm16 (s16le mono 24 kHz) · opus (Ogg/Opus 48 kbit/s). Anything else → 400. |
stream |
bool | true |
true: chunked transfer, first bytes after the first frame; WAV header carries a placeholder length. false: buffered; WAV gets a correct length and Content-Length; Opus is a complete file. |
raw |
bool | false |
Skip the normaliser; the text must already be normalised. |
digits |
string | hi |
Hinglish voices only: hi (Hindi numbers and digit names), mixed (codes/OTPs/phones in English digit names, numbers still Hindi), en (English number words throughout, Indian grouping). English voices ignore it: English (US) voices always read numbers in English with Western grouping, English (IN) voices in English with Indian conventions (double and triple digits, lakh and crore). |
style |
string | none | Expression, saanvi and sahil only: excited · sad · whisper (empty or neutral = the voice as is). A voice without that expression plays neutral, no error; English (US) and English (IN) voices ignore it. Unknown name → 400 unknown style <x>; styles: excited, sad, whisper. See Expressions. |
en_digits |
bool | false |
Legacy alias for "digits": "mixed"; ignored when digits is set. |
postfx |
bool | true |
The output chain: high-pass, loudness to −16 LUFS, 10 ms fade-in, true-peak limiter at −1 dBTP. false returns the decoder's raw frames (what every response was before 2026-10-20). |
profile |
string | default |
default or telephony (adds a 300 Hz high-pass, a +3 dB presence band at 2.5–4 kHz and a de-esser for 8 kHz μ-law/PSTN delivery). Anything else → 400. Ignored when postfx is false. |
temperature |
number | 0.3 |
0–2. Sampling noise; lower is more stable, higher more varied. Out of range → 400. |
seed |
integer | random | Fixes the noise for reproducible output. |
request_id |
string | generated | Echoed as x-request-id, stored in your usage log. |
sample_rate |
integer | 24000 |
Only 24000 is accepted (400 otherwise). |
speed |
number | 1.0 |
Speaking pace, 0.5–2.0: 1.25 is a quarter faster, 0.8 a fifth slower. The pitch and the voice stay the same; only the duration changes (it becomes the natural duration ÷ speed). pace is accepted as an alias. Outside the range → 400. Best between 0.75 and 1.5; beyond that the stretch becomes audible. See Pace. |
Response¶
200 OK, Content-Type audio/wav, audio/L16-style raw PCM (pcm16) or audio/ogg, x-request-id: <id>, Cache-Control: no-store.
With stream: true the body is chunked (Transfer-Encoding: chunked); with stream: false it has Content-Length.
Audio: 24 000 Hz, mono, 16-bit. Frames are 80 ms (1 920 samples); after the first frame they are decoded four at a time, so bytes arrive in ~320 ms steps ahead of real time.
Errors¶
| Status | Body | When |
|---|---|---|
400 |
{"error":"text is empty"} · text: a single word or number group (63 tokens) exceeds the chunk limit of 50 tokens: … · unknown voice …; see /v1/voices · unknown style …; styles: excited, sad, whisper · format must be pcm16, wav or opus · profile must be default or telephony · sample_rate must be 24000 · speed must be between 0.5 and 2 (1.0 = the voice's natural pace) · temperature must be in [0, 2] · nothing to say after normalisation · text: … |
request validation |
401 |
missing x-api-key · invalid api key |
auth |
500 |
chunk 2: frame cap (142 frames for 48 tokens) reached without end-of-speech — generation did not finish the text |
the model did not reach the end of a chunk within its frame budget: nothing is served rather than a looping or cut chunk. Retry with another seed, or shorter sentences; a streamed body aborts at that point instead of ending clean |
402 |
out of credits: top up at https://tts.vartalaap.si/billing (or move to a monthly plan) |
pay-as-you-go balance at zero (credits) |
403 |
voice sahil not allowed for this key |
key's voice allow-list |
413 |
text longer than 4000 characters · text splits into N chunks; max 64 |
size |
503 + Retry-After: 1 |
all workers busy and the queue is full; retry |
capacity (the contract) |
500 |
{"error":"<engine error>"} · worker gone |
engine failure; retry once |
Errors are decided before any audio is sent: a 200 that has started streaming will not turn into an error status. If the engine
fails mid-stream (rare), the body simply ends early; the WebSocket reports the same condition as an error event.
Examples¶
# streamed WAV straight to a player
curl -sN -X POST https://api.vartalaap.si/v1/tts -H "x-api-key: $VARTALAAP_API_KEY" -H 'content-type: application/json' \
-d '{"text":"नमस्ते, electricity repair के लिए आज सुबह बात हुई थी।","voice":"saanvi","format":"wav"}' | ffplay -nodisp -autoexit -i -
# complete Ogg/Opus file with English digits for the code
curl -s -X POST https://api.vartalaap.si/v1/tts -H "x-api-key: $VARTALAAP_API_KEY" -H 'content-type: application/json' \
-d '{"text":"आपका OTP 482913 है.","voice":"sahil","format":"opus","stream":false,"en_digits":true}' -o otp.ogg
# an expression: Saanvi, excited
curl -sN -X POST https://api.vartalaap.si/v1/tts -H "x-api-key: $VARTALAAP_API_KEY" -H 'content-type: application/json' \
-d '{"text":"बधाई हो! आपका loan approve हो गया है.","voice":"saanvi","style":"excited","format":"wav"}' | ffplay -nodisp -autoexit -i -
# raw PCM into ffmpeg → m4a, reproducible with a seed
curl -sN -X POST https://api.vartalaap.si/v1/tts -H "x-api-key: $VARTALAAP_API_KEY" -H 'content-type: application/json' \
-d '{"text":"ठीक है, धन्यवाद.","format":"pcm16","seed":7}' | ffmpeg -f s16le -ar 24000 -ac 1 -i - -c:a aac -b:a 96k out.m4a
import os, requests
r = requests.post("https://api.vartalaap.si/v1/tts", stream=True,
headers={"x-api-key": os.environ["VARTALAAP_API_KEY"]},
json={"text": "Callback +44 00000 19024 पर करें।", "voice": "saanvi", "format": "pcm16"})
if r.status_code == 503:
wait = float(r.headers.get("retry-after", "1")) # nothing was billed; retry after `wait`
r.raise_for_status()
for pcm in r.iter_content(chunk_size=None):
sink.write(pcm) # s16le, 24 kHz, mono
const res = await fetch("https://api.vartalaap.si/v1/tts", { method: "POST",
headers: { "x-api-key": process.env.VARTALAAP_API_KEY, "content-type": "application/json" },
body: JSON.stringify({ text: "Callback +44 00000 19024 पर करें।", voice: "saanvi", format: "pcm16" }) });
if (res.status === 503) retryAfter(Number(res.headers.get("retry-after") || 1));
for await (const pcm of res.body) sink.write(pcm); // Buffer chunks, s16le 24 kHz mono