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

# Authenticate your app

> Keep your API key on your server, give each user a short-lived Dictation-only token, and keep your rewrite prompt off their device.

If your app calls Dictation from your users' devices (a desktop app, a mobile
app or a web page), don't ship your API key in it. Anyone can read a key out of
an app or its network traffic and use it for anything your account can do.

Instead, your server keeps the key and hands each signed-in user a temporary
token when they're about to dictate. The token works in place of your key, only
for Dictation, and expires after a short window. If you send your own rewrite
prompt, save it in your account so the app sends its name instead of the prompt.

**1. Your server mints a token**

When a signed-in user is about to dictate, your server sends your API key to
the token endpoint and gets back a token that can only open Dictation.

**2. Your app dictates with the token**

The app sends the dictation request as usual, with the token in the
`Authorization` header instead of your API key.

**3. The token expires**

The token stops opening new requests after the time you set, so a token
that leaks is worth little.

## Mint a Dictation-only token

Send `POST` to a token host with your API key and an `access` block that turns
off every other product:

```bash theme={null}
curl -X POST "https://token.assemblyai.com/v1/token" \
  -H "Authorization: $ASSEMBLYAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "dictation",
    "expires_in_seconds": 60,
    "access": {
      "streaming": { "enabled": false },
      "sync": { "enabled": false },
      "speech_to_speech": { "enabled": false },
      "tts": { "enabled": false },
      "llm_gateway": { "enabled": false }
    }
  }'
```

```json theme={null}
{ "token": "<temporary token>", "expires_in_seconds": 60 }
```

> [!WARNING]
>
> `"product": "dictation"` on its own doesn't stop the token opening other
> products. Without the `access` block, a token minted for Dictation can also
> call Streaming, Sync, the Voice Agent API, TTS and LLM Gateway, billed to your
> account. Always send the
> `access` block from a token you hand to a user's device.

Turning off `sync` and `llm_gateway` doesn't affect Dictation, which calls them
with its own credentials, not your token. `llm_response`,
`llm_instruction`, `rewrite_mode` and saved prompts work the same with a
Dictation-only token.

Here's the same request from a server, in Python and Node.js:

```python Python theme={null}
import os
import requests

DICTATION_ONLY = {
    "streaming": {"enabled": False},
    "sync": {"enabled": False},
    "speech_to_speech": {"enabled": False},
    "tts": {"enabled": False},
    "llm_gateway": {"enabled": False},
}

def mint_dictation_token() -> str:
    response = requests.post(
        "https://token.assemblyai.com/v1/token",
        headers={"Authorization": os.environ["ASSEMBLYAI_API_KEY"]},
        json={"product": "dictation", "expires_in_seconds": 60, "access": DICTATION_ONLY},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()["token"]
```

```javascript Node.js theme={null}
const DICTATION_ONLY = {
  streaming: { enabled: false },
  sync: { enabled: false },
  speech_to_speech: { enabled: false },
  tts: { enabled: false },
  llm_gateway: { enabled: false },
};

export async function mintDictationToken() {
  const response = await fetch("https://token.assemblyai.com/v1/token", {
    method: "POST",
    headers: {
      Authorization: process.env.ASSEMBLYAI_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ product: "dictation", expires_in_seconds: 60, access: DICTATION_ONLY }),
  });
  if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
  return (await response.json()).token;
}
```

Serve this from an endpoint in your own backend that only your signed-in users
can call. See [Temporary tokens](https://assemblyai.com/docs/temporary-tokens.md) for every field the token
endpoint takes.

## Dictate with the token

On the device, send the dictation request exactly as you would with your API
key, and put the token in the `Authorization` header. Both endpoints accept the
token, and both allow requests from any web origin, so a browser can call
Dictation directly:

```javascript theme={null}
async function dictate(audioBlob, { getToken }) {
  const token = await getToken(); // from your backend's mint endpoint

  const form = new FormData();
  form.append("config", JSON.stringify({}));      // config goes first
  form.append("audio", audioBlob, "audio.wav");

  const response = await fetch("https://dictation.assemblyai.com/v1/transcribe/live", {
    method: "POST",
    headers: { Authorization: token },
    body: form,
  });
  if (!response.ok) throw new Error(`Dictation failed: ${response.status}`);

  const result = await response.json();
  return result.llm_response || result.text; // fall back to the verbatim transcript
}
```

To upload while the user is still speaking, use the same token with the
streaming request in [Achieve lowest latency](https://assemblyai.com/docs/dictation/uploading-while-recording.md).

## Keep tokens off the critical path

Fetching a token is one more round trip, so don't wait to fetch it until the
user starts speaking:

* **Fetch it early.** Mint a token when the user is likely to dictate soon, such
  as when your dictation UI opens or the app comes to the front, and keep it
  until it's close to expiring.
* **Mint a fresh token before it expires.** `expires_in_seconds` is how long a
  token can wait before it opens a request, from 1 to 600. A request that opens
  in time runs to the end, even a long recording on
  [`/v1/transcribe`](https://assemblyai.com/docs/dictation/recordings-over-two-minutes).
* **Keep the window short.** Until it expires, a token can open more than one
  request. A short window limits what a leaked token can do.
* **Pre-warm without a token.** [`GET /warm`](https://assemblyai.com/docs/dictation/connection-pre-warming)
  needs no key or token, so you can warm the connection while the token is still
  on its way.

Mint the token before you start recording. If a streaming request is rejected
with `401` partway through, the audio you already streamed can't be sent again,
so get a fresh token first and avoid starting a dictation on a token that's
about to expire.

## Protect your mint endpoint

Usage through a token is billed to the API key that minted it, and a token
carries nothing about which of your users it was minted for. Your backend is the
only place that knows who asked for each token, so put your controls there:

* Mint tokens only for users who are signed in to your app.
* Rate-limit the endpoint per user, for example a few tokens a minute, which is
  plenty for dictation.
* Log which user each token was minted for, so you can trace unusual usage back
  to an account and block it.

## Keep your prompt off the device

If you rewrite with your own prompt, the prompt travels in every request your
app sends, where anyone inspecting the traffic can read it. Save the prompt in
your account instead, and have the app send only its name.

Create the [saved prompt](https://assemblyai.com/docs/llm-gateway/saved-prompts.md) once, from your server or
as a deploy step, with your API key. Put `{{ text }}` where the transcript goes:

```bash theme={null}
curl -X POST "https://llm-gateway.assemblyai.com/v1/prompts" \
  -H "Authorization: $ASSEMBLYAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-app-cleanup",
    "prompt": "{\"model\": \"gemma-4-31b\", \"max_tokens\": 500, \"messages\": [{\"role\": \"system\", \"content\": \"Clean up this dictation for {{ user_name }}. Remove filler words and fix punctuation. Reply with only the text.\"}, {\"role\": \"user\", \"content\": \"{{ text }}\"}]}"
  }'
```

Then the app refers to it by name in the `llm` field of its dictation config,
with any per-user values in `prompt_replacements`:

```json theme={null}
{
  "llm": {
    "aai_prompt": "my-app-cleanup",
    "prompt_replacements": { "user_name": "Sam" }
  }
}
```

The prompt itself never reaches the device, and you can change it without
shipping a new version of your app. The model, `max_tokens` and other settings
come from the saved prompt, so don't send `rewrite_mode` or other `llm` fields
with it. See [Sending your own LLM request](https://assemblyai.com/docs/dictation/transcript-rewriting.md#sending-your-own-llm-request)
for the details.

## Data zones

Mint the token on the token host for the data zone your app sends dictations to:

| Dictation host | Token host |
| - | - |
| `https://dictation.assemblyai.com` | `https://token.assemblyai.com` |
| `https://dictation.us.assemblyai.com` | `https://token.us.assemblyai.com` |
| `https://dictation.eu.assemblyai.com` | `https://token.eu.assemblyai.com` |

See [Cloud endpoints & data residency](https://assemblyai.com/docs/dictation/endpoints-and-data-zones.md).

## Related pages

* [Temporary tokens](https://assemblyai.com/docs/temporary-tokens.md): every field the token endpoint takes.
* [Transcript rewriting](https://assemblyai.com/docs/dictation/transcript-rewriting.md): instructions, rewrite
  modes and saved prompts.
* [Error handling](https://assemblyai.com/docs/dictation/error-handling.md): status codes and retries.
* [Achieve lowest latency](https://assemblyai.com/docs/dictation/uploading-while-recording.md): upload while
  the user speaks.
