Every call to the speech APIs carries your API key. If you do not have one yet, see API keys.
Where the key goes
The header differs by service. Match it exactly, including capitalisation.
| API | Transport | How the key is sent |
|---|---|---|
| Speech to text | HTTP and WebSocket | X-Api-Key header |
| Text to speech | HTTP | X-API-Key header |
| Text to speech | WebSocket | auth_token field inside the opening hello frame |
Non-streaming transcription
curl -X POST https://stt.navana.ai/api/transcribe \
-H "X-Api-Key: $BODHI_API_KEY" \
-F "model=hi-banking-v2-8khz" \
-F "transaction_id=$(uuidgen)" \
-F "audio_file=@recording.wav"import os, uuid, requests
response = requests.post(
"https://stt.navana.ai/api/transcribe",
headers={"X-Api-Key": os.environ["BODHI_API_KEY"]},
data={
"model": "hi-banking-v2-8khz",
"transaction_id": str(uuid.uuid4()),
},
files={"audio_file": open("recording.wav", "rb")},
)import { openAsBlob } from 'node:fs'
const form = new FormData()
form.set('model', 'hi-banking-v2-8khz')
form.set('transaction_id', crypto.randomUUID())
form.set('audio_file', await openAsBlob('recording.wav'), 'recording.wav')
const res = await fetch('https://stt.navana.ai/api/transcribe', {
method: 'POST',
headers: { 'X-Api-Key': process.env.BODHI_API_KEY! },
body: form,
})file, err := os.Open("recording.wav")
if err != nil {
panic(err)
}
defer file.Close()
var body bytes.Buffer
form := multipart.NewWriter(&body)
form.WriteField("model", "hi-banking-v2-8khz")
form.WriteField("transaction_id", uuid.NewString())
part, _ := form.CreateFormFile("audio_file", "recording.wav")
io.Copy(part, file)
form.Close()
req, _ := http.NewRequest(http.MethodPost,
"https://stt.navana.ai/api/transcribe", &body)
req.Header.Set("X-Api-Key", os.Getenv("BODHI_API_KEY"))
req.Header.Set("Content-Type", form.FormDataContentType())
resp, err := http.DefaultClient.Do(req)Streaming transcription
The same header goes on the WebSocket upgrade request.
import os
from websockets.asyncio.client import connect
async def transcribe():
async with connect(
"wss://stt.navana.ai",
additional_headers={"X-Api-Key": os.environ["BODHI_API_KEY"]},
) as socket:
...import WebSocket from 'ws'
const socket = new WebSocket('wss://stt.navana.ai', {
headers: { 'X-Api-Key': process.env.BODHI_API_KEY! },
})header := http.Header{}
header.Set("X-Api-Key", os.Getenv("BODHI_API_KEY"))
conn, _, err := websocket.DefaultDialer.Dial("wss://stt.navana.ai", header)Non-streaming synthesis
curl -X POST https://tts.navana.ai/tts/bytes \
-H "X-API-Key: $BODHI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"आपका स्वागत है","lang":"hi","output_format":"8000:pcm16"}' \
--output speech.pcmimport os, requests
response = requests.post(
"https://tts.navana.ai/tts/bytes",
headers={
"X-API-Key": os.environ["BODHI_API_KEY"],
"Content-Type": "application/json",
},
json={"text": "आपका स्वागत है", "lang": "hi", "output_format": "8000:pcm16"},
)Streaming synthesis
This one authenticates in the opening frame rather than a header:
await socket.send(json.dumps({
"type": "hello",
"lang": "hi",
"output_format": "24000:pcm16",
"auth_token": os.environ["BODHI_API_KEY"],
}))What the key is not for
Account management, meaning billing, top-ups, usage history, and creating or revoking keys, is handled in the platform app under your login. Those screens are not part of this API and are not authenticated by an API key.
When authentication fails
On transcription, 401 means the key was rejected, 402 means the balance is
exhausted, and 403 means the account is inactive or the key lacks the scope.
See Status codes for authentication
for what to do about each.
Synthesis reports all of those as 401. It does not tell you which one it was,
so check the balance and the key’s status in the app if a key you expect to
work is refused.
Keep the key server-side. See API keys for obtaining, storing, and rotating one.