Realtime TTS-2 is live. Built for realtime conversation that feels human. Learn more

Authentication

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 minted from your API key. It is built for clients that must not hold the key itself — browsers, mobile apps, devices, or gateways running outside your infrastructure. Your backend keeps the API key and mints one token per connection attempt; the client authenticates with the token, once.

One-time tokens are the preferred client credential: every token carries its parent key's full permissions, and a single-use, minutes-lived credential exposes them for at most one connection — a leaked one that was already used is worthless.

Mint a token

Authenticate with your API key — bearer callers are refused, so a token can never mint 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. 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 — hand it to your client.
  • name (tokens/{id}) is the token's stable ID: safe to log and correlate on.

See the API reference 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,
]);
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.

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. The user argument must come from your existing trusted authentication middleware, and canUseSpeech must be a server-checked application permission, never a field accepted from the request body. Apply your app's CSRF protection and per-user rate limit before invoking 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 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 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 the moment it authenticates — a request that later fails (wrong parameters, insufficient permissions) has still consumed it. Nothing un-consumes a token; mint a fresh one per attempt.
  • Scopes are inherited: the token can do exactly what its parent API key can do — no more, no less.
  • Realtime sessions refuse one-time tokens. Use a session token for Realtime, ideally minted from a Realtime-only key; one-time tokens cover direct TTS, STT, and other API connections.
  • No individual revocation: a token dies at its expire_time, or immediately when you delete its parent API key — deleting the key invalidates everything minted from it. To retire one unused token early, spend it yourself: any authenticated call consumes a single-use token, which is as final as expiry.
  • 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.