Ephemeral end-user tokens (eut_) for mobile & headless apps
Mint short-lived, scoped Call2Me tokens with your API key so a mobile client can chat and talk to your agents without ever holding your sk_ key. Usage is billed to your tenant wallet.
Updated July 7, 2026
Ephemeral end-user tokens
When you embed Call2Me in a mobile app or any client you don't fully control, you must not ship your sk_ API key — it grants full account access. Instead, your backend mints a short-lived, scoped ephemeral end-user token (eut_ prefix) per end user and hands that to the client.
How it works
- Your backend calls
POST /v1/end-users/{external_id}/tokenswith yoursk_key. - Call2Me returns an
eut_token (a signed JWT, stateless — no storage on your side). - Your backend passes the token to the mobile client.
- The client uses
Authorization: Bearer eut_...to call chat/voice endpoints directly. - Usage is billed to your wallet (the tenant that owns the
sk_key).
The external_id is your own identifier for the end user (e.g. user_42). It is embedded in the token and cannot be spoofed by the client.
Minting a token
curl -X POST https://api.call2me.app/v1/end-users/user_42/tokens \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"expires_in": 3600,
"scopes": ["chat", "voice"],
"agent_ids": ["agent_abc"]
}'
Response:
{
"token": "eut_eyJhbGciOi...",
"expires_in": 3600,
"scopes": ["chat", "voice"],
"external_id": "user_42"
}
Request fields
| Field | Type | Default | Notes |
|---|---|---|---|
expires_in | int (seconds) | 3600 | Clamped to [60, 86400] (24h max). |
scopes | string[] | ["chat","voice","agents:read"] | Any of chat, voice, agents:read. Unknown scope → 400. |
agent_ids | string[] | null | null | Restrict the token to these agents. null = all agents. |
What an eut_ token can call
An eut_ token is restricted to a fixed allowlist. Everything else returns 403.
| Method & path | Required scope |
|---|---|
POST /v1/chats* (create session, send message) | chat |
POST /v1/voice/sessions | voice |
GET /v1/voice/sessions/{room} | voice |
POST /v1/audio/tts, POST /v1/audio/stt | chat or voice |
GET /v1/agents, GET /v1/agents/{id} (public card) | agents:read |
An eut_ token cannot mint more tokens, read your wallet, manage agents, or touch any config endpoint — those all return 403.
SDK
from call2me import Call2Me
c = Call2Me("sk_live_your_key")
tok = c.end_users.create_token("user_42", scopes=["chat", "voice"], expires_in=3600)
# hand tok["token"] to your mobile client
const { Call2Me } = require("@call2me/sdk");
const c = new Call2Me("sk_live_your_key");
const tok = await c.endUsers.createToken("user_42", { scopes: ["chat", "voice"] });
Tying sessions to your users (external_user_id)
Chat and voice sessions accept an optional external_user_id (your own identifier for the end user) plus a free-form metadata object:
curl -X POST https://api.call2me.app/v1/chats \
-H "Authorization: Bearer eut_..." \
-d '{"agent_id": "agent_abc", "metadata": {"mood": "anxious"}}'
- With an
eut_token,external_user_idis taken from the token automatically — the client cannot spoof another user's id, so you don't need to (and shouldn't) send it in the body. - With your
sk_key (server-side), passexternal_user_idexplicitly to attribute the session, then query them back:GET /v1/chats?external_user_id=user_42. - An
eut_token'sGET /v1/chatsis always scoped to its ownexternal_user_id— one end user can never list another's sessions.
Listing agents (public cards)
To build an agent picker in your app, first attach public card metadata to each
agent (with your sk_ key):
curl -X PATCH https://api.call2me.app/v1/agents/agent_abc \
-H "Authorization: Bearer sk_live_your_key" \
-d '{"public_metadata": {"emoji": "🧘", "title": {"en": "Meditation", "tr": "Meditasyon"}, "order": 1, "visible": true}}'
Then the mobile client (with an eut_ token, agents:read scope) lists them —
and receives only trimmed public cards:
curl https://api.call2me.app/v1/agents -H "Authorization: Bearer eut_..."
[
{ "agent_id": "agent_abc", "name": "Zen Coach", "language": "en-US",
"voice_id": "v_calm", "public_metadata": { "emoji": "🧘", "title": {"en": "Meditation"}, "order": 1, "visible": true } }
]
The system prompt, knowledge base, webhook and any cost/config fields are never
returned to an ephemeral token. Agents with public_metadata.visible: false are
hidden, and results are ordered by public_metadata.order.
Reading a voice session back (transcript)
After a voice session ends, fetch its detail + transcript by room name:
curl https://api.call2me.app/v1/voice/sessions/agent_x_ab12 \
-H "Authorization: Bearer eut_..."
{
"room_name": "agent_x_ab12",
"status": "ended",
"duration_sec": 143.2,
"cost_usd": 0.24,
"transcript": [
{ "role": "user", "text": "I feel anxious today" },
{ "role": "agent", "text": "Let's take a slow breath together." }
],
"external_user_id": "user_42"
}
With an eut_ token, this only returns sessions belonging to that end user —
one user can never read another's transcript (mismatches return 404). cost_usd
populates once the session's usage has been reported.
Security notes
- Tokens are stateless signed JWTs — there is no server-side revocation. Keep
expires_inshort (default 1h) and re-mint as needed. - The
external_idis bound into the token; the client cannot impersonate another user. - A leaked
eut_token can only spend your wallet on chat/voice until it expires — it can never escalate to full account access.
Frequently asked
Q.Why not just ship my sk_ API key in the mobile app?
An sk_ key grants full access to your account. If it leaks from a mobile binary, an attacker controls your tenant. Ephemeral tokens (eut_) are short-lived, scoped to chat/voice only, and can never mint more tokens — so a leak is low-impact and self-expiring.
Q.Who pays for usage made with an eut_ token?
You — the tenant whose sk_ key minted the token. All chat/voice/audio usage accrues to your wallet, exactly as if you made the call yourself.
Q.Can an end user see my agent's system prompt or other tenants' data?
No. An eut_ token can only reach a fixed allowlist of endpoints (chat, voice, audio, and read-only public agent cards). Everything else returns 403.