---
title: "Authentication"
description: "How the platform API key authenticates your calls, and where it goes in each request."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.navana.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

Every call to the speech APIs carries your API key. If you do not have one yet,
see [API keys](/api-keys).

## Where the key goes

Send the key in an `X-API-Key` header. The same header works on both services
and both transports:

| 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 | `X-API-Key` header, or an `auth_token` field in the opening `hello` frame |

Text-to-speech streaming takes the key in the `hello` frame as well, for clients
that cannot set headers on a WebSocket handshake — a browser's `WebSocket`
constructor, for one.

### Non-streaming transcription

```bash
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"
```

```python
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")},
)
```

```ts
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,
})
```

```go
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.

```python
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:
    ...
```

```ts
import WebSocket from 'ws'

const socket = new WebSocket('wss://stt.navana.ai', {
  headers: { 'X-Api-Key': process.env.BODHI_API_KEY! },
})
```
```go
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

```bash
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.pcm
```

```python
import 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:

```python
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](/api-keys#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](/api-keys) for obtaining, storing,
and rotating one.

Source: https://docs.navana.ai/authentication/index.mdx
