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:
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"
}'{
"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_usemust betrue— only single-use tokens are supported today.ttlorexpire_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.accessTokenis 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:
const ws = new WebSocket("wss://api.inworld.ai/tts/v1/voice:streamBidirectional", [
"bearer_" + accessToken,
]);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.
- The client signs in to your application.
- 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.
- Return only
accessTokenandexpireTime, withCache-Control: no-store. - The client uses
accessTokenonce. 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.
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):
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.