Authentication
Session tokens
Deprecated multi-use JWTs — use one-time tokens wherever possible.
A session token is a JWT minted from your API key. Unlike a one-time token, it is multi-use: the client can open connections and make requests with it repeatedly until it expires, a few hours after minting. For new direct client integrations, use it for a Realtime 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.
Mint a session token
POST https://api.inworld.ai/v1/sessionTokens/token:generateRequests to this endpoint are authenticated with a signature computed from your API key — not by sending the key in the header directly — so the secret is not sent in the request; the key ID is included. The server-side example below implements the signing for this endpoint. Keep both the ID and secret on your backend.
The request body:
{
"api_key": "<your key id>",
"resources": ["workspaces/my-workspace"]
}api_key— the key ID the request is signed with.resources(optional) — a singleworkspaces/{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:
{
"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.
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. 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 and WebRTC for connection URLs and session messages. The TTS SDK refreshes tokens for you — pass a fetcher and it re-mints shortly before expiry:
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 minting — always read
expirationTimerather 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 — shorter-lived, and dead after first use. See Security best practices for choosing between the two.