API reference
Every endpoint, field and limit in the Dhvni AI API, with examples you can paste. Text to speech and speech to text for Hindi, English and Hinglish, over HTTPS and WebSockets.
- Base URL
- https://api.dhvni.in
- Authentication
- Bearer API key
- Text to speech model
- dhvni-voice-1
- Speech to text model
- dhvni-transcribe-1
Introduction
Dhvni AI does two things. Text to speech turns text into audio in a voice set up for your account. Speech to text turns a recording into text. Both handle Hindi, English and Hinglish, including sentences that switch languages halfway.
Every request goes to https://api.dhvni.in over HTTPS. You send JSON, except for recordings, which go up as multipart form data. Audio comes back as the audio itself, ready to save or play.
Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/tts | Turn text into audio, as a file or a stream |
WS /v1/tts/stream | Speak text while it is still being written |
POST /v1/stt | Turn a recording into text |
GET /v1/voices | List the voices on your account |
GET /v1/models | List the models |
GET /v1/usage | Read the characters and seconds you have used |
POST /v1/tokens | Get a two minute token for a browser WebSocket |
Quickstart
Three steps from no account to your first audio file.
Get an API key
Request access and tell us what you are building. We create your account and send you an API key, shown once. Keep it on your server, for example in an environment variable.
Shellexport DHVNI_API_KEY="paste your key here"Find your voice id
Our team sets up each voice with the speaker's consent and adds it to your account. List your voices to get its id.
cURLcurl https://api.dhvni.in/v1/voices \ -H "Authorization: Bearer $DHVNI_API_KEY"Speak your first line
Send text with the voice id. The audio comes back as an mp3 file.
cURLcurl https://api.dhvni.in/v1/tts \ -H "Authorization: Bearer $DHVNI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "voice_id": "voice_7f3a91c2d4e85b60", "text": "नमस्ते, आज हम photosynthesis पढ़ेंगे।" }' \ -o hello.mp3
Authentication
Send your API key as a bearer token in the Authorization header of every request.
Authorization: Bearer $DHVNI_API_KEYKeep the key on your server. Anyone who has it can use your account, so never put it in a web page or in an app you ship. If a key leaks, write to support@dhvni.in so we can turn it off and issue a new one.
Browser tokens
POST/v1/tokens
Browsers cannot set headers on a WebSocket, so a web page cannot send your key to WS /v1/tts/stream. Instead, your backend asks for a short lived token and passes it to the page, which connects with ?token=.
curl -X POST https://api.dhvni.in/v1/tokens \
-H "Authorization: Bearer $DHVNI_API_KEY"{
"token": "vgs_...",
"expires_at": 1793059320
}| Field | Description |
|---|---|
token | Pass it as the token query parameter when you open WS /v1/tts/stream. |
expires_at | When the token stops working, in unix seconds. Two minutes after it was issued. |
A token lasts two minutes and only opens the streaming WebSocket; every other endpoint needs the API key. Ask for a fresh token for each connection.
Voices
GET/v1/voices
Our team sets up voices with the speaker's consent and adds them to your account; in this version you do not create them through the API. List them to fill a voice picker or to find a voice_id. The newest voice comes first.
curl https://api.dhvni.in/v1/voices \
-H "Authorization: Bearer $DHVNI_API_KEY"{
"voices": [
{
"id": "voice_7f3a91c2d4e85b60",
"name": "Kavya, teacher",
"speaker_name": "Kavya Sharma",
"status": "active",
"shared": false,
"reference_seconds": 1264.8,
"created_at": 1792966000.41,
"activated_at": 1793052400.87
}
]
}Voice fields
| Field | Type | Description |
|---|---|---|
id | string | Pass this as voice_id. |
name | string | The name we gave the voice, ready for a voice picker. |
speaker_name | string | The person whose voice it is. |
status | string | Always active in this list. A voice appears once it is ready to use. |
shared | boolean | true for a voice open to every account, false for one made for yours. |
reference_seconds | number | Length of the recording the voice was built from, in seconds. |
created_at | number | When the voice was added, in unix seconds. |
activated_at | number | When the voice went live, in unix seconds. |
Need another voice? See voice cloning for what to send us.
Text to speech
POST/v1/tts
Send text and a voice id as JSON. The audio comes back as one file or, with stream set to true, as a stream that starts in about 0.3 seconds and keeps coming while it is generated.
Request body
| Field | Default | Description |
|---|---|---|
voice_idstring | Required | The voice to speak in, from GET /v1/voices. |
textstring | Required | What to say, 1 to 5,000 characters. Write Hindi in Devanagari, English words in English letters, and numbers as words. See writing text for Hindi speech. |
modelstring | dhvni-voice-1 | The text to speech model. It is the only one in v1, so you can leave it out. |
formatstring | mp3 | mp3, opus, wav or pcm. See audio formats. |
sample_rateinteger | 24000 | 16000, 22050, 24000, 44100 or 48000. |
qualitystring | fast | fast for live replies. hq is slightly higher quality and a little slower, for audio you render ahead of time. |
streamboolean | false | true sends audio while it is generated. The first audio arrives in about 0.3 seconds. |
curl https://api.dhvni.in/v1/tts \
-H "Authorization: Bearer $DHVNI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"voice_id": "voice_7f3a91c2d4e85b60",
"text": "देखो बेटा, mitochondria को cell का powerhouse कहते हैं।",
"format": "mp3",
"quality": "hq"
}' \
-o lesson.mp3Audio formats
Every format is mono, at the sample_rate you ask for.
| Format | Content type | Notes |
|---|---|---|
mp3 | audio/mpeg | 64 kbps mono. Plays in every browser and phone. |
opus | audio/ogg | Opus in an Ogg container, 32 kbps mono. The smallest files. |
wav | audio/wav | 16 bit mono PCM with a header. In a stream, the header's length fields hold their largest value because the length is not known yet; most players accept this. |
pcm | audio/pcm | Raw 16 bit little endian mono samples at sample_rate, with no header. The simplest to feed to a player chunk by chunk. |
Response
A 200 response body is the audio itself, with these headers.
| Header | Sent | Value |
|---|---|---|
Content-Type | Always | The content type of the format you asked for. |
X-Request-Id | Always | An id such as req_4c1e9a7b2f6d8e03. Include it when you write to us about a request. |
X-Audio-Seconds | Complete files | Length of the audio in seconds, to two decimals. Streams do not carry it, because the length is not known when the headers go out. |
HTTP/1.1 200 OK
content-type: audio/mpeg
x-request-id: req_4c1e9a7b2f6d8e03
x-audio-seconds: 4.86The 200 status of a stream goes out with the first audio, so it cannot change later. If generation fails partway, the stream ends early. Retry the request if the audio stops before the text does.
Text to speech is metered by the character of text. Spaces and punctuation count, and in Devanagari each vowel sign counts on its own, so बेटा is four characters. Rates are on the pricing page.
Streaming text to speech
WS/v1/tts/stream
Use the WebSocket when text arrives a piece at a time, for example from your own LLM. Send text as it is written; each sentence is spoken as soon as it is complete, while the next one is still on its way. Audio comes back as raw PCM frames.
wss://api.dhvni.in/v1/tts/stream?voice_id=voice_7f3a91c2d4e85b60&sample_rate=24000&quality=fastConnecting
| Field | Default | Description |
|---|---|---|
voice_idstring | Required | The voice to speak in. |
sample_rateinteger | 24000 | 16000, 22050, 24000, 44100 or 48000. |
qualitystring | fast | fast or hq, as for POST /v1/tts. |
tokenstring | Optional | A browser token from POST /v1/tokens. From a server, send the Authorization header instead. |
From a server, send the Authorization header as usual. From a browser, get a token from your backend and add it as token. There is no format here: audio is always 16 bit PCM at sample_rate.
Messages you send
Send each message as a JSON text frame.
| Message | Effect |
|---|---|
{"type": "text", "text": "..."} | Adds text. Each sentence is spoken as soon as it is complete. |
{"type": "flush"} | Speaks whatever is buffered now, even half a sentence. Send it at the end of a reply. |
{"type": "cancel"} | Stops speaking and drops everything queued. The connection stays open for new text. |
{"type": "close"} | Speaks what is still queued, sends done, then closes the connection. |
A sentence is complete when its end mark (., !, ? or ।) is followed by a space or a new line, so a decimal such as 3.14 is never split. The first piece may stop at a comma so the first audio comes sooner. After that, short sentences are grouped so the intonation does not restart every few words.
Messages you receive
| Message | Meaning |
|---|---|
{"type": "ready", "request_id", "model", "sample_rate"} | Connected and ready for text. |
| Binary frames | Audio, in order: 16 bit little endian mono PCM at sample_rate. |
{"type": "chunk.done", "text", "audio_ms"} | One piece of text has been spoken. text is that piece and audio_ms its length. |
{"type": "cancelled"} | Your cancel has taken effect. |
{"type": "done", "characters", "audio_ms"} | Everything queued has been spoken, after a close. Totals for the connection; it closes next. |
{"type": "error", "message"} | Something went wrong. If the voice is not on your account or a limit is reached, it also carries status (404 or 429) and the connection closes. |
An unknown type, or a frame that is not JSON, gets an error message and the connection stays open. If the connection cannot be accepted, it closes with one of these codes.
| Code | Meaning |
|---|---|
4401 | The API key or token is missing, wrong or expired. |
4400 | sample_rate or quality is not one of the allowed values. |
Example
// 1. Get a token from your backend (see Authentication)
const { token } = await fetch("/voice-token", { method: "POST" }).then((r) => r.json());
// 2. Connect
const ws = new WebSocket(
`wss://api.dhvni.in/v1/tts/stream?voice_id=voice_7f3a91c2d4e85b60&sample_rate=24000&token=${token}`
);
ws.binaryType = "arraybuffer";
ws.onmessage = (e) => {
if (typeof e.data !== "string") return player.write(e.data); // PCM16 audio
const msg = JSON.parse(e.data);
if (msg.type === "error") console.error(msg.message);
};
// 3. Send text as your LLM writes it, then close
ws.onopen = async () => {
for await (const delta of llm.stream(prompt)) {
ws.send(JSON.stringify({ type: "text", text: delta }));
}
ws.send(JSON.stringify({ type: "close" }));
};Every character you send in a text message is metered, including text you later cancel. An open connection counts as one of your four text to speech requests at once until it closes.
Speech to text
POST/v1/stt
Upload a recording as multipart form data and get the transcript back. Hindi comes back in Devanagari and English words in English letters, the way the sentence was spoken.
Form fields
| Field | Default | Description |
|---|---|---|
filefile | Required | The recording: mp3, mp4, m4a, wav, webm, ogg or flac, up to 25 MB. Keep the extension in the file name. |
modelstring | dhvni-transcribe-1 | The speech to text model. It is the only one in v1, so you can leave it out. |
languagestring | Optional | hi or en when the whole recording is in one language. Leave it out for Hinglish. |
promptstring | Optional | Names and terms to expect, for example NCERT, mitochondria, Priya Ma'am. It helps those words come back spelled your way. |
response_formatstring | json | json for the object below, or text for the transcript alone as plain text. |
curl https://api.dhvni.in/v1/stt \
-H "Authorization: Bearer $DHVNI_API_KEY" \
-F file=@question.m4a \
-F prompt="NCERT, mitochondria, Priya Ma'am"Response
{
"text": "सर, mitochondria को powerhouse क्यों कहते हैं?",
"model": "dhvni-transcribe-1",
"duration_seconds": 3.2,
"request_id": "req_9b2d41e7c05a6f38"
}| Field | Type | Description |
|---|---|---|
text | string | The transcript, with Hindi in Devanagari and English words in English letters. |
model | string | dhvni-transcribe-1. |
duration_seconds | number | Length of the audio in seconds. This is what is metered. |
request_id | string | The same id as the X-Request-Id header. |
With response_format set to text, the body is the transcript alone, as plain text. The X-Request-Id header is set either way.
Speech to text is metered by the second of audio, as reported in duration_seconds. Rates are on the pricing page.
Models and usage
Models
GET/v1/models
Lists the two models: dhvni-voice-1 for text to speech and dhvni-transcribe-1 for speech to text. Each endpoint has one model, so you can leave model out of every request.
curl https://api.dhvni.in/v1/models \
-H "Authorization: Bearer $DHVNI_API_KEY"{
"models": [
{
"id": "dhvni-voice-1",
"type": "text-to-speech",
"languages": ["hi", "en", "hinglish"]
},
{
"id": "dhvni-transcribe-1",
"type": "speech-to-text",
"languages": ["hi", "en", "hinglish"]
}
]
}Usage
GET/v1/usage
Totals for each metered unit over a window of time. Without parameters, the window is the last 30 days.
| Field | Default | Description |
|---|---|---|
sincenumber | 30 days before until | Start of the window, in unix seconds. |
untilnumber | Now | End of the window, in unix seconds. |
curl "https://api.dhvni.in/v1/usage?since=1790467200&until=1793059200" \
-H "Authorization: Bearer $DHVNI_API_KEY"{
"since": 1790467200.0,
"until": 1793059200.0,
"usage": [
{ "metric": "stt_seconds", "quantity": 8214.6, "unit": "seconds" },
{ "metric": "tts_characters", "quantity": 1482306.0, "unit": "characters" },
{ "metric": "tts_seconds", "quantity": 96120.45, "unit": "seconds" }
]
}| Metric | Unit | Counts |
|---|---|---|
tts_characters | characters | Characters of text sent to speech. Text to speech is billed on this. |
tts_seconds | seconds | Seconds of audio generated. |
stt_seconds | seconds | Seconds of audio transcribed. Speech to text is billed on this. |
A metric with no usage in the window is left out. Usage is recorded when each request or stream finishes, and quantities are rounded to two decimals.
Errors and limits
Errors come back as JSON with a detail message that says what went wrong.
{
"detail": "Voice voice_7f3a91c2d4e85b60 not found on this account."
}| Status | When | What to do |
|---|---|---|
400 | The input cannot be used: an unknown model, text with nothing to speak, an empty file, or audio that cannot be processed. | Fix the request; detail says what is wrong. |
401 | The API key is missing or wrong. | Check the Authorization header. |
404 | The voice is not on your account. | Use an id from GET /v1/voices. |
413 | Text longer than 5,000 characters, or a file larger than 25 MB. | Split the text at sentence ends, or trim or compress the audio. |
422 | A field is missing, or its value is not one of the allowed ones, such as a sample_rate of 32000. | detail is a list with one entry per field at fault. |
429 | Too many requests in a minute, or too many at once. | Wait the number of seconds in Retry-After, or a few seconds if it is not set, then retry. |
500 | Speech generation failed. | Retry once. If it keeps happening, write to us with the X-Request-Id. |
502 | Transcription failed. | Retry once. If it keeps happening, write to us with the X-Request-Id. |
503 | The service is busy or briefly unavailable. | Retry after a short wait, using Retry-After when it is present. |
When a field fails validation, the status is 422 and detail is a list, with one entry per field: loc says where the field is and msg what is wrong with it.
{
"detail": [
{
"type": "literal_error",
"loc": ["body", "sample_rate"],
"msg": "Input should be 16000, 22050, 24000, 44100 or 48000",
"input": 32000
}
]
}Limits
| Limit | Value | Applies to |
|---|---|---|
| Requests | 120 a minute | Text to speech requests, streaming connections and speech to text requests, counted together. |
| Text to speech at once | 4 | POST /v1/tts and open WS /v1/tts/stream connections together. A stream keeps its place until it ends. |
| Speech to text at once | 8 | POST /v1/stt. |
| Text per request | 5,000 characters | POST /v1/tts. |
| Audio per upload | 25 MB | POST /v1/stt. |
Limits are per account. Over a limit, you get 429 with a Retry-After header in seconds; on the WebSocket, the same limits arrive as an error message with status 429. If you need more room, write to support@dhvni.in.
// Retries a request when the API asks you to wait.
async function withRetry(send, tries = 3) {
for (let attempt = 1; ; attempt++) {
const res = await send();
if (![429, 503].includes(res.status) || attempt === tries) return res;
const seconds = Number(res.headers.get("retry-after") ?? 2);
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
}
const res = await withRetry(() => fetch("https://api.dhvni.in/v1/tts", options));Writing text for Hindi speech
Dhvni reads Hinglish the way it is written, so how you write the text decides how it sounds. Three habits make the biggest difference.
Hindi in Devanagari, English in English letters
Keep each word in the script of its language. English terms are then said the way an Indian speaker says them.
Write: देखो बेटा, mitochondria cell का powerhouse है।
Avoid: Dekho beta, mitochondria cell ka powerhouse hai.
Hindi typed in English letters picks up an English accent.
Numbers as words
Write numbers, dates and ranges the way they should be said, in the language of the sentence around them.
Write: आपका refund तीन से पाँच working days में आ जाएगा।
Avoid: आपका refund 3-5 working days में आ जाएगा।
Digits and symbols leave the reading to chance.
Finish each sentence
End sentences with । or a full stop, and questions with a question mark. Text is split into pieces at these marks before it is spoken.
Write: आपकी EMI पाँच तारीख को due है। क्या मैं payment link भेज दूँ?
Avoid: आपकी EMI पाँच तारीख को due है क्या मैं payment link भेज दूँ
Without end marks, two sentences read as one.
Also worth knowing
- Markdown symbols such as
*,#and_, list bullets and emoji are removed before speaking, so you can pass LLM output straight in. - Spaces and punctuation count as characters, and so does each Devanagari vowel sign: बेटा is four characters. See pricing.
- Transcripts follow the same rules: Hindi in Devanagari, English words in English letters. Pass names and subject words in
promptto get them spelled your way.