Skip to content

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

Endpoints in v1
EndpointWhat it does
POST /v1/ttsTurn text into audio, as a file or a stream
WS /v1/tts/streamSpeak text while it is still being written
POST /v1/sttTurn a recording into text
GET /v1/voicesList the voices on your account
GET /v1/modelsList the models
GET /v1/usageRead the characters and seconds you have used
POST /v1/tokensGet a two minute token for a browser WebSocket

Quickstart

Three steps from no account to your first audio file.

  1. 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.

    Shell
    export DHVNI_API_KEY="paste your key here"
  2. 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.

    cURL
    curl https://api.dhvni.in/v1/voices \
      -H "Authorization: Bearer $DHVNI_API_KEY"
  3. Speak your first line

    Send text with the voice id. The audio comes back as an mp3 file.

    cURL
    curl 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.

Header
Authorization: Bearer $DHVNI_API_KEY

Keep 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"
Response
{
  "token": "vgs_...",
  "expires_at": 1793059320
}
Response from POST /v1/tokens
FieldDescription
tokenPass it as the token query parameter when you open WS /v1/tts/stream.
expires_atWhen 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
curl https://api.dhvni.in/v1/voices \
  -H "Authorization: Bearer $DHVNI_API_KEY"
Response
{
  "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

Fields of each voice
FieldTypeDescription
idstringPass this as voice_id.
namestringThe name we gave the voice, ready for a voice picker.
speaker_namestringThe person whose voice it is.
statusstringAlways active in this list. A voice appears once it is ready to use.
sharedbooleantrue for a voice open to every account, false for one made for yours.
reference_secondsnumberLength of the recording the voice was built from, in seconds.
created_atnumberWhen the voice was added, in unix seconds.
activated_atnumberWhen 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

Request body for POST /v1/tts
FieldDefaultDescription
voice_idstringRequiredThe voice to speak in, from GET /v1/voices.
textstringRequiredWhat 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.
modelstringdhvni-voice-1The text to speech model. It is the only one in v1, so you can leave it out.
formatstringmp3mp3, opus, wav or pcm. See audio formats.
sample_rateinteger2400016000, 22050, 24000, 44100 or 48000.
qualitystringfastfast for live replies. hq is slightly higher quality and a little slower, for audio you render ahead of time.
streambooleanfalsetrue 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.mp3

Audio formats

Every format is mono, at the sample_rate you ask for.

Audio formats for POST /v1/tts
FormatContent typeNotes
mp3audio/mpeg64 kbps mono. Plays in every browser and phone.
opusaudio/oggOpus in an Ogg container, 32 kbps mono. The smallest files.
wavaudio/wav16 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.
pcmaudio/pcmRaw 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.

Response headers from POST /v1/tts
HeaderSentValue
Content-TypeAlwaysThe content type of the format you asked for.
X-Request-IdAlwaysAn id such as req_4c1e9a7b2f6d8e03. Include it when you write to us about a request.
X-Audio-SecondsComplete filesLength of the audio in seconds, to two decimals. Streams do not carry it, because the length is not known when the headers go out.
Response headers
HTTP/1.1 200 OK
content-type: audio/mpeg
x-request-id: req_4c1e9a7b2f6d8e03
x-audio-seconds: 4.86

The 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.

URL
wss://api.dhvni.in/v1/tts/stream?voice_id=voice_7f3a91c2d4e85b60&sample_rate=24000&quality=fast

Connecting

Query parameters for WS /v1/tts/stream
FieldDefaultDescription
voice_idstringRequiredThe voice to speak in.
sample_rateinteger2400016000, 22050, 24000, 44100 or 48000.
qualitystringfastfast or hq, as for POST /v1/tts.
tokenstringOptionalA 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.

Messages you send on WS /v1/tts/stream
MessageEffect
{"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

Messages you receive on WS /v1/tts/stream
MessageMeaning
{"type": "ready", "request_id", "model", "sample_rate"}Connected and ready for text.
Binary framesAudio, 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.

WebSocket close codes
CodeMeaning
4401The API key or token is missing, wrong or expired.
4400sample_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

Form fields for POST /v1/stt
FieldDefaultDescription
filefileRequiredThe recording: mp3, mp4, m4a, wav, webm, ogg or flac, up to 25 MB. Keep the extension in the file name.
modelstringdhvni-transcribe-1The speech to text model. It is the only one in v1, so you can leave it out.
languagestringOptionalhi or en when the whole recording is in one language. Leave it out for Hinglish.
promptstringOptionalNames and terms to expect, for example NCERT, mitochondria, Priya Ma'am. It helps those words come back spelled your way.
response_formatstringjsonjson 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

Response
{
  "text": "सर, mitochondria को powerhouse क्यों कहते हैं?",
  "model": "dhvni-transcribe-1",
  "duration_seconds": 3.2,
  "request_id": "req_9b2d41e7c05a6f38"
}
Response from POST /v1/stt
FieldTypeDescription
textstringThe transcript, with Hindi in Devanagari and English words in English letters.
modelstringdhvni-transcribe-1.
duration_secondsnumberLength of the audio in seconds. This is what is metered.
request_idstringThe 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
curl https://api.dhvni.in/v1/models \
  -H "Authorization: Bearer $DHVNI_API_KEY"
Response
{
  "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.

Query parameters for GET /v1/usage
FieldDefaultDescription
sincenumber30 days before untilStart of the window, in unix seconds.
untilnumberNowEnd of the window, in unix seconds.
cURL
curl "https://api.dhvni.in/v1/usage?since=1790467200&until=1793059200" \
  -H "Authorization: Bearer $DHVNI_API_KEY"
Response
{
  "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" }
  ]
}
Usage metrics
MetricUnitCounts
tts_characterscharactersCharacters of text sent to speech. Text to speech is billed on this.
tts_secondssecondsSeconds of audio generated.
stt_secondssecondsSeconds 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.

Error
{
  "detail": "Voice voice_7f3a91c2d4e85b60 not found on this account."
}
Error statuses
StatusWhenWhat to do
400The 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.
401The API key is missing or wrong.Check the Authorization header.
404The voice is not on your account.Use an id from GET /v1/voices.
413Text longer than 5,000 characters, or a file larger than 25 MB.Split the text at sentence ends, or trim or compress the audio.
422A 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.
429Too 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.
500Speech generation failed.Retry once. If it keeps happening, write to us with the X-Request-Id.
502Transcription failed.Retry once. If it keeps happening, write to us with the X-Request-Id.
503The 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.

Validation error
{
  "detail": [
    {
      "type": "literal_error",
      "loc": ["body", "sample_rate"],
      "msg": "Input should be 16000, 22050, 24000, 44100 or 48000",
      "input": 32000
    }
  ]
}

Limits

Limits per account
LimitValueApplies to
Requests120 a minuteText to speech requests, streaming connections and speech to text requests, counted together.
Text to speech at once4POST /v1/tts and open WS /v1/tts/stream connections together. A stream keeps its place until it ends.
Speech to text at once8POST /v1/stt.
Text per request5,000 charactersPOST /v1/tts.
Audio per upload25 MBPOST /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.

JavaScript
// 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 prompt to get them spelled your way.