> ## 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 with a temporary token

> Mint a short-lived token on your server so a browser or an app can open a Streaming TTS session without your API key.

A page or an app that talks to the Streaming TTS API directly must not carry your API key: the key would be in the shipped code, in URLs, and in whatever logs those URLs reach. Mint a temporary token on your server instead, hand it to the client, and let the client open its session with that. The token is good for one short window, usage through it is attributed to the key that minted it, and a token cannot mint another token.

Browsers cannot set headers on a WebSocket handshake, which is why the API also accepts the credential as a `token` query parameter. That parameter takes an API key too, but never send one from a device you do not control.

**1. Mint a token on your server**

`POST https://token.us.assemblyai.com/v1/token` with your API key in the `Authorization` header and a JSON body with two fields, both required: `product`, which is `"tts"`, and `expires_in_seconds`, how long the token can wait before it opens a session. An optional `access` object limits what the token can open; see [Temporary tokens](https://assemblyai.com/docs/temporary-tokens.md).

> [tab] Python
>
> ```python theme={null}
> import os
>
> import requests
>
> # One session per process keeps the connection to the token host open between mints.
> http = requests.Session()
>
>
> def create_temporary_token():
>     response = http.post(
>         "https://token.us.assemblyai.com/v1/token",
>         json={"product": "tts", "expires_in_seconds": 60},
>         headers={"Authorization": os.environ["ASSEMBLYAI_API_KEY"]},
>     )
>     response.raise_for_status()
>     return response.json()["token"]
> ```
>
> The Python SDK has no minting helper for Streaming TTS; the request above is all it takes.

> [tab] JavaScript
>
> ```javascript theme={null}
> async function createTemporaryToken() {
>   const response = await fetch("https://token.us.assemblyai.com/v1/token", {
>     method: "POST",
>     headers: {
>       Authorization: process.env.ASSEMBLYAI_API_KEY,
>       "Content-Type": "application/json",
>     },
>     body: JSON.stringify({ product: "tts", expires_in_seconds: 60 }),
>   });
>   if (!response.ok) throw new Error(`token request failed: ${response.status}`);
>
>   const data = await response.json();
>   return data.token;
> }
> ```

> [tab] JavaScript SDK
>
> ```javascript theme={null}
> import { AssemblyAI } from "assemblyai";
>
> const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY });
>
> async function createTemporaryToken() {
>   return client.tts.createTemporaryToken({ expires_in_seconds: 60 });
> }
> ```
>
> The SDK mints on the token host of the zone its `ttsBaseUrl` is in, so a client set up for the EU host gets an EU token.

> [tab] cURL
>
> ```bash theme={null}
> curl -X POST "https://token.us.assemblyai.com/v1/token" \
>   -H "Authorization: $ASSEMBLYAI_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d '{"product": "tts", "expires_in_seconds": 60}'
> ```

The answer is JSON with the token in its `token` field, a long opaque string:

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

* **`expires_in_seconds`**, 1 to 600, is the window in which the token can open a session. Past it the connection is refused with `1008` and `Unauthorized: Signature has expired`. A session that is already open is not affected when its token expires.
* **`max_session_duration_seconds`**, optional and at least 60, caps the session opened with the token. The cap shows up as `expires_at` on the `Begin` frame, and the session closes with code `3008` when it is reached. A session never lasts longer than an hour either way.
* **Zones.** `token.us.assemblyai.com` and `token.eu.assemblyai.com` serve the same route within the US and EU data zones, and a token from either opens sessions on both TTS hosts. `token.assemblyai.com` serves it too, from the nearest region of either zone. Mint on the token host of the zone your sessions connect to, so that the minting request is processed in that zone too; see [Cloud endpoints and data residency](https://assemblyai.com/docs/tts/endpoints-and-data-zones.md).
* **Errors.** A key the route refuses, or a request it cannot accept, gets a `4xx` answer with the reason in the JSON `detail` field. A `500` or `502` means the route could not issue a token just then: retry with backoff.
* **Latency.** Minting adds a request to your server and one to the token host before the session can open, and a mint over a new connection also waits for the TCP and TLS handshakes. Mint before the client needs the token, for example when the page loads or while the user is still speaking, within the `expires_in_seconds` window, and reuse one HTTP connection on your server for every mint.

> [!WARNING]
>
> **Mint a new token for every connection, reconnects included, and keep
> `expires_in_seconds` short.** A token is not single-use: until it expires,
> it opens as many sessions and HTTP requests as it is given to, so a token
> that leaks from a page can be used by someone else. Never put your API key in the `token` parameter of a page you
> ship. The query string is part of the URL, and URLs are written to server
> access logs, browser history and developer tools.

**2. Open the session with the token**

The client fetches the token from your server, then passes it in place of the API key. On the WebSocket API a browser sends it in the `token` query parameter, and a client that can set headers may send it in the `Authorization` header instead, raw or after `Bearer `. Send it in one place only: a credential in the header wins over `token`, and a header that is empty or only `Bearer` carries none; see [Authentication](https://assemblyai.com/docs/tts/connect-parameters.md#authentication). In the fragments below, `fetchToken()` and `fetch_token()` are requests to your own server, and `play()` is whatever takes a chunk of audio in your app.

> [tab] Python SDK
>
> ```python theme={null}
> import assemblyai as aai
>
> config = aai.TTSConfig(voice_id="37e65973-9d96-5aab-adcd-01bf56ab5af9")  # Kyla
>
> with aai.TTSSession(config, token=fetch_token()) as session:
>     for chunk in session.speak("Hello from a device without an API key."):
>         play(chunk)
> ```
>
> Give each connection a token of its own: to reconnect, mint a new token and build a new session with it. The session opens on the US host unless `aai.settings.tts_base_url` names the EU one; see [Cloud endpoints and data residency](https://assemblyai.com/docs/tts/endpoints-and-data-zones.md).

> [tab] JavaScript SDK
>
> ```javascript theme={null}
> import { TextToSpeechSession } from "assemblyai";
>
> const session = new TextToSpeechSession({
>   token: await fetchToken(),
>   voiceId: "37e65973-9d96-5aab-adcd-01bf56ab5af9", // Kyla
> });
> await session.connect();
>
> for await (const chunk of session.speak("Hello from a device without an API key.")) {
>   play(chunk);
> }
> await session.close();
>
> // Later, a new connection takes a new token.
> await session.connect({ token: await fetchToken() });
> ```
>
> Building the session directly, with no client, keeps the API key out of the page. It connects to the US host unless you pass `websocketBaseUrl`, the whole WebSocket URL, such as `wss://tts.eu.assemblyai.com/v1/ws`.

> [tab] JavaScript (browser)
>
> ```javascript theme={null}
> const params = new URLSearchParams({
>   voice_id: "37e65973-9d96-5aab-adcd-01bf56ab5af9",
>   language: "english",
>   sample_rate: "24000",
>   encoding: "pcm_s16le",
>   token: await fetchToken(),
> });
> const ws = new WebSocket(`wss://tts.us.assemblyai.com/v1/ws?${params}`);
> ```
>
> Mint a fresh token for every connection attempt, reconnects included.

> [tab] Python (WebSocket)
>
> ```python theme={null}
> from urllib.parse import urlencode
>
> params = {
>     "voice_id": "37e65973-9d96-5aab-adcd-01bf56ab5af9",
>     "language": "english",
>     "sample_rate": 24000,
>     "encoding": "pcm_s16le",
>     "token": fetch_token(),
> }
> url = f"wss://tts.us.assemblyai.com/v1/ws?{urlencode(params)}"
> ```

A token that was refused, because it expired or is not a valid token, closes the session with `1008` and `error_type` `unauthorized` before `Begin`. See [Error handling](https://assemblyai.com/docs/tts/error-handling.md).

## Where else a token works

A temporary token works wherever an API key does, in the `Authorization` header of the [HTTP API](https://assemblyai.com/docs/tts/api-reference/http-api/overview.md), so a page can call it directly too. Under a token, `max_session_duration_seconds` also caps how long an HTTP request may run. See [HTTP API basics](https://assemblyai.com/docs/tts/api-reference/http-api/overview.md#authentication).

### Speak a whole text over HTTP from a page

A page with a whole text to speak and no need for barge-in can call the HTTP API directly, one `fetch` per text, played as it streams in. Every route answers cross-origin requests from any origin. Send the token in the `Authorization` header like a key; the HTTP API reads the credential from that header only. A token stays valid for any number of requests until it expires, so the page can retry a failed request with the same token.

The client below schedules each piece of audio on one `AudioContext` as it arrives, so playback starts with the first bytes. Create and resume the context in a user gesture, and reuse it for every request. `/tts-token` is a route on your server that returns a freshly minted token as JSON.

```javascript theme={null}
const ctx = new AudioContext({ sampleRate: 24000 });

document.getElementById("speak").onclick = async () => {
  await ctx.resume();
  await speak("Hello there. How can I help?", await getToken());
};

async function getToken() {
  const response = await fetch("/tts-token");
  return (await response.json()).token;
}

async function speak(text, token) {
  const res = await fetch("https://tts.us.assemblyai.com/v1/speech", {
    method: "POST",
    headers: { Authorization: token, "Content-Type": "application/json" },
    body: JSON.stringify({ text }),
  });
  if (!res.ok) {
    const isJson = (res.headers.get("Content-Type") ?? "").startsWith("application/json");
    const detail = isJson ? (await res.json()).code : await res.text();
    throw new Error(`${res.status}: ${detail}`);
  }
  const rate = Number(res.headers.get("X-TTS-Sample-Rate"));
  let at = ctx.currentTime + 0.05;
  let carry = new Uint8Array(0);
  const reader = res.body.getReader();
  for (;;) {
    const { value, done } = await reader.read(); // rejects with a TypeError when the response is cut
    if (done) break;
    const bytes = new Uint8Array(carry.length + value.length);
    bytes.set(carry);
    bytes.set(value, carry.length);
    const whole = bytes.length & ~1; // whole 16-bit samples only
    carry = bytes.slice(whole);
    if (whole === 0) continue;
    const pcm = new Int16Array(bytes.buffer, 0, whole / 2);
    const buffer = ctx.createBuffer(1, pcm.length, rate);
    buffer.copyToChannel(Float32Array.from(pcm, (s) => s / 32768), 0);
    const source = ctx.createBufferSource();
    source.buffer = buffer;
    source.connect(ctx.destination);
    at = Math.max(at, ctx.currentTime);
    source.start(at);
    at += buffer.duration;
  }
}
```

Every response header can be read from a page, errors included, so `X-Request-Id` and the `X-TTS-*` headers are available to your error reporting.

## Related

* [WebSocket API basics](https://assemblyai.com/docs/tts/connect-parameters.md#authentication): the `token` parameter.
* [Optimizing accuracy and latency](https://assemblyai.com/docs/tts/getting-started/optimizing-accuracy-and-latency.md#in-a-browser): minting the next token before the page needs it.
* [Limits](https://assemblyai.com/docs/tts/limits.md): the session duration cap a token can set.
* [Streaming Speech-to-Text temporary tokens](https://assemblyai.com/docs/streaming/authenticate-with-a-temporary-token.md): the same pattern for speech-to-text, on its own token route.
