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

# Session tokens

> Deprecated multi-use JWTs — use one-time tokens wherever possible.

<Warning>Session tokens are deprecated. Prefer [one-time tokens](https://docs.inworld.ai/portal/ephemeral-tokens.md) for new direct TTS, STT, and LLM client connections. Direct browser/mobile [Realtime](https://docs.inworld.ai/realtime/overview.md) connections still require a session token. Server-side Realtime connections can use an API key directly.</Warning>

A session token is a JWT created from your API key. Unlike a [one-time token](https://docs.inworld.ai/portal/ephemeral-tokens.md), it can be used repeatedly. The client can open connections and make requests until the token expires, a few hours after creation. For new direct client integrations, use it for a [Realtime](https://docs.inworld.ai/realtime/overview.md) voice session and mint it on your backend, so the API key itself never reaches the client. A long-lived TTS WebSocket can use a one-time token: single-use limits connection attempts, not the number of synthesis messages on that connection.

<Warning>A session token inherits all permissions assigned to its parent API key. It can call every API the key reaches throughout the token's lifetime, any number of times. Use [key permission management](https://docs.inworld.ai/portal/api-keys.md#key-permissions) to choose the narrowest key that fits, such as a [Realtime-only key](https://docs.inworld.ai/portal/api-keys.md#realtime-only-api-keys) for Realtime sessions. Keep write toggles off unless needed. Wherever one connection per credential fits, prefer a [one-time token](https://docs.inworld.ai/portal/ephemeral-tokens.md) instead.</Warning>

## Mint a session token

```
POST https://api.inworld.ai/v1/sessionTokens/token:generate
```

This endpoint authenticates requests with a signature computed from your API key, rather than the key sent directly in a header. The request includes the key ID but not the secret. The server-side example below implements the signing for this endpoint. Keep both the ID and secret on your backend.

The request body:

```json
{
  "api_key": "<your key id>",
  "resources": ["workspaces/my-workspace"]
}
```

- `api_key` — the key ID the request is signed with.
- `resources` (optional) — a single `workspaces/{workspace}` name to scope the token to. Omitted, the token is scoped to the key's own workspace. Another workspace is allowed when the key's workspace collection contains it, or when it is public.

The response:

```json
{
  "token": "eyJhbGciOiJSUzI1NiIs...",
  "type": "Bearer",
  "expirationTime": "2026-09-01T16:00:00Z",
  "sessionId": "my-workspace:1f0d8e9a-..."
}
```

### Server-side mint example

Save this as `mint-session-token.mjs` on your Node.js backend. It reads the same copied Base64 `INWORLD_API_KEY` used for direct requests, decodes it **once** to obtain the separate signing inputs, and returns the minted token. Decoding here is specific to the signed mint; do not decode the credential before a Basic-auth request.

```javascript
import { createHmac, randomBytes } from 'node:crypto';

export async function mintSessionToken() {
  const credential = process.env.INWORLD_API_KEY;
  if (!credential) throw new Error('Missing server API credential');
  const parts = Buffer.from(credential, 'base64').toString('utf8').split(':');
  if (parts.length !== 2 || !parts[0] || !parts[1]) {
    throw new Error('Expected the copied Base64 key ID and secret');
  }
  const [keyId, secret] = parts;
  const host = 'api.inworld.ai';
  const method = 'ai.inworld.engine.v1.SessionTokens/GenerateSessionToken';
  const datetime = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
  const nonce = randomBytes(16).toString('hex');
  let signature = Buffer.from('IW1' + secret, 'utf8');
  for (const part of [datetime, host, method, nonce, 'iw1_request']) {
    signature = createHmac('sha256', signature).update(part).digest();
  }
  const authorization = `IW1-HMAC-SHA256 ApiKey=${keyId},DateTime=${datetime},Nonce=${nonce},Signature=${signature.toString('hex')}`;
  const response = await fetch(`https://${host}/v1/sessionTokens/token:generate`, {
    method: 'POST',
    headers: {
      Authorization: authorization,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ api_key: keyId }),
    signal: AbortSignal.timeout(10000),
  });
  if (!response.ok) throw new Error(`Session mint failed (${response.status})`);
  return response.json();
}
```

Call `mintSessionToken()` inside your application's authenticated, authorized, rate-limited token endpoint, following the [backend handler pattern](https://docs.inworld.ai/portal/ephemeral-tokens.md#connect-a-client-through-your-backend). Return only `token` and `expirationTime` with `Cache-Control: no-store`. Keep the workspace choice on the server; omitting `resources` above uses the key's workspace. Generate a new signature for each attempt and keep your server clock synchronized.

## Use it

Send the token as a Bearer credential from the client:

```
Authorization: Bearer <token>
```

It works across the APIs allowed by the parent key, including WebSocket and Realtime connections. Browser WebSockets use `new WebSocket(url, ["bearer_" + token])`; WebRTC signaling uses the Bearer HTTP header. See [Realtime WebSocket](https://docs.inworld.ai/realtime/connect/websocket.md) and [WebRTC](https://docs.inworld.ai/realtime/connect/webrtc.md) for connection URLs and session messages. The TTS SDK refreshes tokens for you — pass a fetcher and it re-mints shortly before expiry:

```javascript
const tts = InworldTTS({
  token: await fetchToken(),        // your backend endpoint that mints the token
  onTokenExpiring: fetchToken,      // called automatically before expiry
});
```

## Lifetime and revocation

- Tokens expire a few hours after creation. Always read `expirationTime` rather than assuming a duration, and refresh before it passes.
- A session token is valid until it expires: there is no per-token revocation. Deleting the parent API key invalidates every token minted from it immediately.

For clients that open exactly one connection per credential, prefer [one-time tokens](https://docs.inworld.ai/portal/ephemeral-tokens.md) — shorter-lived, and dead after first use. See [Security best practices](https://docs.inworld.ai/portal/auth-security.md) for choosing between the two.
