Quickstart¶
From zero to a WAV file in about a minute, then to streaming audio in a few more.
1. Get a key¶
Sign in at tts.vartalaap.si (e-mail magic link, no password), open API keys and create one. The
full key is shown once; it looks like vt_live_ followed by 32 characters. Put it in an environment variable:
export VARTALAAP_API_KEY="vt_live_…"
$env:VARTALAAP_API_KEY = "vt_live_…"
Every request carries it as x-api-key: <key> (or Authorization: Bearer <key>). A key can be limited to specific voices; see
Authentication & keys.
2. First call: a WAV file¶
curl -sN -X POST https://api.vartalaap.si/v1/tts \
-H "x-api-key: $VARTALAAP_API_KEY" -H "content-type: application/json" \
-d '{"text":"नमस्ते, मैं सरल finserv से प्रिया बोल रही हूँ. आपका OTP 4821 है.","voice":"anjura","format":"wav"}' \
-o hello.wav
import os, requests # pip install requests
r = requests.post(
"https://api.vartalaap.si/v1/tts",
headers={"x-api-key": os.environ["VARTALAAP_API_KEY"]},
json={"text": "नमस्ते, मैं सरल finserv से प्रिया बोल रही हूँ. आपका OTP 4821 है.", "voice": "anjura", "format": "wav"},
stream=True,
)
r.raise_for_status()
with open("hello.wav", "wb") as f:
for chunk in r.iter_content(chunk_size=None): # bytes arrive as frames are decoded
f.write(chunk)
print("request id", r.headers["x-request-id"])
import { createWriteStream } from "node:fs";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
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: "नमस्ते, मैं सरल finserv से प्रिया बोल रही हूँ. आपका OTP 4821 है.", voice: "anjura", format: "wav" }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
await pipeline(Readable.fromWeb(res.body), createWriteStream("hello.wav"));
Play it: ffplay hello.wav, or open it. You'll hear "O T P चार आठ दो एक": the normaliser read the code digit by digit. Set
"en_digits": true to hear four eight two one instead.
Streaming WAV has a placeholder length
With stream: true (the default) the WAV header is written before the length is known; players cope, strict libraries may
not. Use "stream": false for a buffered file with a correct header, or pcm16 / opus for streaming pipelines.
3. Pipe audio 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":"ठीक है, 10 नवंबर तक का time note कर लिया है.","voice":"raj","format":"pcm16"}' \
| ffplay -nodisp -autoexit -f s16le -ar 24000 -ac 1 -i -
pcm16 is raw signed 16-bit little-endian mono at 24 kHz, the format to use when you decode or mix yourself. opus is an Ogg/Opus
stream at 48 kbit/s for telephony and the web.
4. Stream a conversation over WebSocket¶
One connection, many utterances, server-side timing for each. Full event reference: WS /v1/tts/stream.
import asyncio, json, os, websockets # pip install websockets
async def main():
headers = {"x-api-key": os.environ["VARTALAAP_API_KEY"]}
async with websockets.connect("wss://api.vartalaap.si/v1/tts/stream", additional_headers=headers) as ws:
for line in ["नमस्ते, मैं सरल finserv से प्रिया बोल रही हूँ.", "आपकी EMI ₹12,450 है, जो 5 नवंबर को due है."]:
await ws.send(json.dumps({"text": line, "voice": "anjura", "format": "pcm16"}))
done = 0
async for msg in ws:
if isinstance(msg, bytes):
pass # 16-bit PCM @ 24 kHz: play or buffer it
else:
ev = json.loads(msg)
if ev["type"] == "first_audio_ms": print("first audio after", ev["ms"], "ms")
if ev["type"] == "end":
print("utterance done:", ev["audio_s"], "s, rtf", ev["rtf"]); done += 1
if done == 2: break
if ev["type"] == "error": print("error", ev); break
asyncio.run(main())
import WebSocket from "ws"; // npm i ws
const ws = new WebSocket("wss://api.vartalaap.si/v1/tts/stream", { headers: { "x-api-key": process.env.VARTALAAP_API_KEY } });
ws.on("open", () => ws.send(JSON.stringify({ text: "आपका payment receive हो गया है, धन्यवाद.", voice: "raj", format: "pcm16" })));
ws.on("message", (data, isBinary) => {
if (isBinary) return player.write(data); // 16-bit PCM @ 24 kHz
const ev = JSON.parse(data.toString());
if (ev.type === "first_audio_ms") console.log("first audio after", ev.ms, "ms");
if (ev.type === "end" || ev.type === "error") ws.close();
});
// Browsers cannot set headers on a WebSocket handshake, so the key goes in the URL.
// Mint a short-lived key from your own back end rather than shipping a live key to the page.
const ws = new WebSocket(`wss://api.vartalaap.si/v1/tts/stream?api_key=${key}`);
ws.binaryType = "arraybuffer";
const ctx = new AudioContext({ sampleRate: 24000 }); let at = 0;
ws.onopen = () => ws.send(JSON.stringify({ text, voice: "anjura", format: "pcm16" }));
ws.onmessage = (ev) => {
if (typeof ev.data === "string") return console.log(JSON.parse(ev.data));
const i16 = new Int16Array(ev.data), buf = ctx.createBuffer(1, i16.length, 24000);
buf.getChannelData(0).set(Float32Array.from(i16, (s) => s / 32768));
const src = ctx.createBufferSource(); src.buffer = buf; src.connect(ctx.destination);
at = Math.max(at, ctx.currentTime + 0.03); src.start(at); at += buf.duration; // gapless scheduling
};
5. Watch it in the portal¶
Every request appears under Usage in tts.vartalaap.si with its engine-side time to first audio and real-time factor, and the Playground streams with a live first-audio timer so you can compare REST and WebSocket from your own network.
Next¶
- Streaming guide: what to buffer, how to cancel, barge-in.
- Text normalisation: write
₹2.5 lakh,15/11/2026,1800-209-4567and let the engine say it right. - Limits, errors & 503: the retry contract.
- Paste the agent prompt from the portal's home page into Claude Code or Cursor to have it wire all of this for you.