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

# One-time tokens

> Single-use, short-lived bearer tokens for clients that must never hold your API key.

A one-time token is a single-use, short-lived bearer credential created from your API key. Use it for clients that must not hold the key itself, such as browsers, mobile apps, devices, or gateways outside your infrastructure. Your backend keeps the API key and creates one token per connection attempt. The client authenticates with the token once.

One-time tokens are the preferred client credential. Every token inherits all permissions assigned to its parent API key, but a single-use token that expires in minutes exposes them for at most one connection. A leaked token cannot be used again if it has already been used.

## Mint a token

Authenticate with your [API key](https://docs.inworld.ai/portal/api-key-auth.md). Bearer callers are refused, so a token can never create another token:

```bash
curl https://api.inworld.ai/auth/v1/tokens \
  --request POST \
  --header "Authorization: Basic $INWORLD_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "single_use": true,
    "ttl": "300s",
    "client_reference_id": "user-4711-session-42"
  }'
```

```json
{
  "name": "tokens/1f0d8e9a-1d2b-4c3d-8e9f-0a1b2c3d4e5f",
  "accessToken": "eyJhbGciOiJSUzI1NiIs...",
  "expireTime": "2026-09-01T12:05:00Z",
  "singleUse": true,
  "clientReferenceId": "user-4711-session-42",
  "createTime": "2026-09-01T12:00:00Z",
  "apiKey": "workspaces/my-workspace/apikeys/4aa6d7e9-..."
}
```

- `single_use` must be `true` — only single-use tokens are supported today.
- `ttl` or `expire_time` (pick one): how long the token stays valid, up to 1 hour. If omitted, it defaults to 15 minutes. Keep it as short as your flow allows, just long enough for the client to receive the token and open its connection.
- `client_reference_id` (optional): your own correlation ID (a user or session ID; printable ASCII, at most 256 characters). It is echoed in the response and attached to the token.
- `accessToken` is returned only here and never stored. Send it to your client.
- `name` (`tokens/{id}`) is the token's stable ID: safe to log and correlate on.

See the [API reference](https://docs.inworld.ai/api-reference/authAPI/tokens/create-token.md) for the full schema.

## Use it

The token authenticates the same APIs your key can reach. Send it as a Bearer token:

```
Authorization: Bearer <accessToken>
```

For WebSocket connections from browsers (where you can't set headers), pass it in the `Sec-WebSocket-Protocol` header instead:

```javascript
const ws = new WebSocket("wss://api.inworld.ai/tts/v1/voice:streamBidirectional", [
  "bearer_" + accessToken,
]);
```

<Warning>Do not put the token in a URL query parameter. URLs land in server logs and `Referer` headers, and a logged single-use token can be spent by whoever reads it first.</Warning>

## Connect a client through your backend

Your application needs its own token endpoint. The browser must not call the Inworld mint directly, because that call requires the long-lived API credential.

1. The client signs in to **your application**.
2. Your backend verifies the user's session and permission to use the feature, applies per-user limits, then calls the Inworld mint with its server-only key.
3. Return only `accessToken` and `expireTime`, with `Cache-Control: no-store`.
4. The client uses `accessToken` once. On reconnect or retry, start at step 2 with a fresh token.

For example, mount this Node.js handler at `POST /api/inworld-token` in your backend. Get the `user` argument from your existing trusted authentication middleware. Check the `canUseSpeech` application permission on the server; never accept it from the request body. Apply your app's CSRF protection and per-user rate limit before calling the handler. This example uses Node's built-in `fetch`.

```javascript
export async function issueInworldToken(req, res, user) {
  res.setHeader('Cache-Control', 'no-store');
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST');
    res.writeHead(405).end();
    return;
  }
  if (!user) {
    res.writeHead(401).end();
    return;
  }
  if (!user.canUseSpeech) {
    res.writeHead(403).end();
    return;
  }

  try {
    const apiKey = process.env.INWORLD_API_KEY;
    if (!apiKey) throw new Error('Missing server API credential');
    const upstream = await fetch('https://api.inworld.ai/auth/v1/tokens', {
      method: 'POST',
      headers: {
        Authorization: `Basic ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ single_use: true, ttl: '300s' }),
      signal: AbortSignal.timeout(10000),
    });
    if (!upstream.ok) {
      // Record only the status; never log credentials or the token response.
      console.error('Inworld token mint status:', upstream.status);
      res.writeHead(upstream.status === 429 ? 429 : 502).end();
      return;
    }
    const { accessToken, expireTime } = await upstream.json();
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ accessToken, expireTime }));
  } catch {
    res.writeHead(502).end();
  }
}
```

Then, from a signed-in browser on the same origin as your backend (include your app's CSRF header if required):

```javascript
async function connectSpeech() {
  const response = await fetch('/api/inworld-token', {
    method: 'POST',
    credentials: 'same-origin',
    cache: 'no-store',
  });
  if (!response.ok) {
    throw new Error(`Your app could not issue a token (${response.status})`);
  }
  const { accessToken } = await response.json();
  return new WebSocket(
    'wss://api.inworld.ai/tts/v1/voice:streamBidirectional',
    ['bearer_' + accessToken],
  );
}

const ws = await connectSpeech();
// Send synthesis messages after 'open'; see the WebSocket guide below.
// Call connectSpeech() again for a new attempt; do not reuse accessToken.
```

The [TTS WebSocket guide](https://docs.inworld.ai/tts/synthesize-speech-websocket.md) supplies the synthesis messages and audio handling. For a single HTTP request instead, send the returned `accessToken` in `Authorization: Bearer ...`; mint another token before the next request. Mobile clients follow the same sequence using your application's login mechanism.

For Realtime, use the [session-token mint](https://docs.inworld.ai/portal/session-tokens.md#mint-a-session-token) in your protected backend endpoint instead. Return `token` and `expirationTime`, and pass `token` as the Bearer credential. Do not reuse the one-time mint for Realtime.

## Semantics to design around

- **One use means one authentication**: one HTTP request, or one WebSocket connection, however long it stays open. The token is consumed as soon as it authenticates. A later failure, such as wrong parameters or insufficient permissions, does not restore it. Create a fresh token for every attempt.
- **Scopes are inherited**: the token can do exactly what its parent API key can do, no more and no less.
- **Realtime sessions refuse one-time tokens.** Use a [session token](https://docs.inworld.ai/portal/session-tokens.md) for Realtime, ideally minted from a [Realtime-only key](https://docs.inworld.ai/portal/api-keys.md#realtime-only-api-keys); one-time tokens cover direct TTS, STT, and other API connections.
- **No individual revocation**: a token expires at its `expire_time`, or immediately when you delete its parent API key. Deleting the key invalidates every token created from it. To retire one unused token early, use it yourself: any authenticated call consumes a single-use token permanently.
- **Minting is rate-limited per API key** (60 tokens per minute). If a mint fails with a server error, mint a fresh token rather than retrying a possibly-consumed one.

## Usage attribution

The token ID (`name`) and your `client_reference_id` are attached to requests the token makes, so usage can be attributed to the exact client session that spent it.
