Authentication
The 2Chat Voice SDK never sees your long-lived X-User-API-Key. Instead, your backend exchanges that key for a short-lived JWT scoped to a single 2Chat User, and that JWT is what the browser holds.
Why this matters
- Your
X-User-API-Keystays on a trusted server. - Each browser tab is scoped to one User UUID — perfect for multi-agent softphones.
- Tokens expire (default 1 hour, max 24 hours) so a leaked JWT is short-lived.
- You can rotate tokens transparently with
device.updateToken(...)— no re-registration.
Mint an access token
Endpoint
POST https://api.p.2chat.io/sdk/voip/access-token
Authentication
Server-to-server only. Send your account's X-User-API-Key — never ship this to the browser.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
user_uuid | string | yes | UUID of the 2Chat User this token represents (USR…). Each token binds to one user. Discover UUIDs via GET /open/users. |
label | string | no | Free-form label echoed back in the token claims (e.g. agent-ada, softphone-tab-1). Useful for telemetry. |
ttl | integer | no | Lifetime in seconds. Clamped to [60, 86400]. Default 3600 (1 hour). |
Invocation
- cURL
- Node.js
- Python
- PHP
curl --location --request POST 'https://api.p.2chat.io/sdk/voip/access-token' \
--header 'X-User-API-Key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"user_uuid": "USR48a1c2f0-9d6b-4c2a-8e3f-1a7b9d0c4e22",
"label": "agent-ada",
"ttl": 1800
}'
const res = await fetch("https://api.p.2chat.io/sdk/voip/access-token", {
method: "POST",
headers: {
"X-User-API-Key": process.env.TWO_CHAT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
user_uuid: "USR48a1c2f0-9d6b-4c2a-8e3f-1a7b9d0c4e22",
label: "agent-ada",
ttl: 1800,
}),
});
const { token, expires_at } = await res.json();
import os, requests
res = requests.post(
"https://api.p.2chat.io/sdk/voip/access-token",
headers={
"X-User-API-Key": os.environ["TWO_CHAT_API_KEY"],
"Content-Type": "application/json",
},
json={
"user_uuid": "USR48a1c2f0-9d6b-4c2a-8e3f-1a7b9d0c4e22",
"label": "agent-ada",
"ttl": 1800,
},
timeout=10,
)
data = res.json()
token, expires_at = data["token"], data["expires_at"]
$ch = curl_init('https://api.p.2chat.io/sdk/voip/access-token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-User-API-Key: ' . getenv('TWO_CHAT_API_KEY'),
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'user_uuid' => 'USR48a1c2f0-9d6b-4c2a-8e3f-1a7b9d0c4e22',
'label' => 'agent-ada',
'ttl' => 1800,
]));
$data = json_decode(curl_exec($ch), true);
$token = $data['token'];
Response
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user_uuid": "USR48a1c2f0-9d6b-4c2a-8e3f-1a7b9d0c4e22",
"label": "agent-ada",
"expires_at": "2026-05-08T14:30:00+00:00"
}
| Field | Type | Description |
|---|---|---|
success | boolean | true on success. |
token | string | The signed JWT to hand to the browser. |
user_uuid | string | The User UUID the token is bound to. |
label | string | null | The label you passed, echoed back. |
expires_at | string (ISO 8601) | Absolute expiry time of the token (UTC). |
Errors follow the standard API error format. A missing or unknown user_uuid returns 400 / 404; a bad API key returns 401.
Refresh strategy
The SDK never refreshes tokens on its own — your backend is the source of truth. The recommended pattern:
device.on("tokenWillExpire", async () => {
const { token } = await fetch("/my/backend/voice-token").then((r) => r.json());
await device.updateToken(token);
});
updateToken() swaps the token in place. The new token must be for the same user_uuid — switching users requires a new Device.
Adjust the warning window with tokenExpiryLeadMs on DeviceOptions (default 60_000 ms).
Multiple agents, multiple tabs
Each JWT is bound to one 2Chat User. To run N concurrent agents:
- Provision N Users in your 2Chat account.
- Mint one token per user per session.
- Spin up one
Deviceper token in its own browser context.
A single User cannot be registered from two browsers at the same time — the second register replaces the first.
Security checklist
- ✅ Mint tokens on the server, never the browser.
- ✅ Store
X-User-API-Keyin environment variables / secrets manager. - ✅ Treat the JWT like any short-lived bearer token — HTTPS only.
- ✅ Use the shortest reasonable TTL (
ttl) for your use case. - ❌ Don't embed
X-User-API-Keyin client-side JavaScript. - ❌ Don't reuse one token across multiple users — claims and SIP identity won't match.