Client tokens
Client tokens
An ephemeral, single‑call credential your backend mints and hands to an untrusted client — a browser, a mobile app, an Electron window — so that client can start one voice call without ever holding your API key.
Use this whenever a call starts on a device you don't control and you need it tied to a specific user, account, or context. If your page is fully public and anonymous, the public widget is simpler and needs no backend at all.
Why not just use the API key?
An API key is a bearer credential for your entire organization: every agent, every call record, every phone number. Anything shipped to a browser is public — devtools, view-source, a proxy, a determined user. A leaked key can dial out on your account until you notice and revoke it.
A client token inverts that. It is worth almost nothing to steal:
| API key | Client token | |
|---|---|---|
| Scope | Whole organization | One agent |
| Lifetime | Until revoked | Minutes (10 by default) |
| Calls | Unlimited | 1 by default |
| Context variables | Caller chooses | Fixed at mint time |
| Safe in a browser | No | Yes |
The flow
your server ──X-API-Key──▶ POST /api/client-tokens ──▶ { token, expiresAt }
│
└── token ──▶ browser ──Bearer──▶ POST /api/sessions/init-web-call
│
▼
{ sessionId, websocketUrl } ──▶ connect
Your server decides who this caller is and what the agent should know. The browser only gets permission to start that one call.
Mint a token
POST /api/client-tokens
Auth: your org API key (role owner, admin, or developer) or a user JWT. Never call this endpoint from a browser — it's the thing that protects the key.
curl -X POST https://api.telenow.ai/api/client-tokens \
-H "X-API-Key: vai_live_…" \
-H "Content-Type: application/json" \
-d '{
"agentId": "cce90897-5108-45bf-b849-693f0ec352b3",
"ttlSeconds": 600,
"maxCalls": 1,
"variables": { "customerName": "Asha", "planTier": "gold" },
"callerIdentity": { "identifier": "acct_8891", "channel": "web" }
}'
Request
| Field | Type | Default | Notes |
|---|---|---|---|
agentId | uuid | required | Must belong to your org, else 403. Validated at mint time so failures surface on your server, not in the browser. |
ttlSeconds | int | 600 | Clamped to 30–3600. Gates starting the call, not its duration — a call already underway isn't cut off when the token expires. |
maxCalls | int | 1 | Clamped to 1–100. For retry/reconnect flows, not bulk dialing. |
variables | object | {} | String→string context variables baked into the token. |
callerIdentity | object | — | Trusted caller identity (number, identifier, channel) injected into tool calls. |
Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"expiresAt": "2026-07-25T10:42:00Z"
}
Flat JSON — no {success, data} envelope.
Redeem a token
The client sends it as a Bearer token to init-web-call. The official browser SDK does this for you:
import { TelenowCall } from '@telenow/client-web';
const { token } = await fetch('/api/voice-token').then((r) => r.json()); // your endpoint
const call = new TelenowCall({ token });
await call.start();
Raw HTTP, if you're not using the SDK:
curl -X POST https://api.telenow.ai/api/sessions/init-web-call \
-H "Authorization: Bearer eyJhbGciOi…" \
-H "Content-Type: application/json" -d '{}'
The body can be empty: everything that matters is already in the token.
What the token pins
This is the security contract. At redemption these are read from the token, and anything the client sent in the request body is discarded:
agentId— a holder cannot switch to a different (or more expensive) agent.variables— a holder cannot inject or overwrite prompt context. If you bakedplanTier: "gold", they cannot make itenterprise.callerIdentity— cannot be forged, so tools can trust it for lookups.userId— the session is attributed to the user whose key minted the token.- Softphone mode is refused outright (
403) — a client token can never place an outbound PSTN call.
maxCalls is counted atomically server‑side, so two simultaneous redemptions can't both slip through on the last use.
Errors
| Status | Meaning | Fix |
|---|---|---|
401 | Invalid or expired client token | Past expiresAt, malformed, or signed for a different environment. Mint a fresh one. |
403 | This client token has already been used the maximum number of times. | Raise maxCalls, or mint per call. |
403 | A client token cannot start a softphone call. | Use an API key server‑side for mode: "manual". |
403 | Agent does not belong to this organization | At mint time — check agentId. |
404 | Agent not found | At mint time — check agentId. |
Practical guidance
- Mint per call, on demand. Don't cache tokens; they're cheap. An endpoint like
POST /api/voice-tokenon your own server, protected by your own session auth, is the whole integration. - Put your authorization in the minting endpoint. Telenow checks the token is valid; only you know whether this logged‑in user is allowed to call this agent with this account id. Decide that before you mint.
- Keep TTLs short. The default 10 minutes covers a mic‑permission prompt and a slow connection. Longer just widens the window if a token leaks from a log or a shared screen.
- Don't log tokens. They're credentials for their short life. Redact them like passwords.
ttlSecondsdoesn't limit call length. For that, use the agent's own max‑duration setting.
See also
- Authentication — the full credential matrix
- Web-call API & WebSocket — what to do with
sessionId+websocketUrl - Web SDK —
TelenowCall, which handles redemption and audio - Server SDKs —
telenow.clientTokens.create()