Skip to content

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.

request.http
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

tts.sh
# 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
tts.py
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
tts.mjs
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