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

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 |

> **Two details that are easy to get wrong**
>
> Speech-to-text uses `X-Api-Key` and text-to-speech HTTP uses `X-API-Key`. The
> two differ in capitalisation, so a header copied from one service may not be
> recognised by the other.
>
> Text-to-speech streaming takes no auth header. Its key travels inside the
> first frame you send, not the handshake.

### 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.dev.navana.ai/authentication/index.mdx
