Call2Me
Developers

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

  1. Your backend calls POST /v1/end-users/{external_id}/tokens with your sk_ key.
  2. Call2Me returns an eut_ token (a signed JWT, stateless — no storage on your side).
  3. Your backend passes the token to the mobile client.
  4. The client uses Authorization: Bearer eut_... to call chat/voice endpoints directly.
  5. 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

FieldTypeDefaultNotes
expires_inint (seconds)3600Clamped to [60, 86400] (24h max).
scopesstring[]["chat","voice","agents:read"]Any of chat, voice, agents:read. Unknown scope → 400.
agent_idsstring[] | nullnullRestrict 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 & pathRequired scope
POST /v1/chats* (create session, send message)chat
POST /v1/voice/sessionsvoice
GET /v1/voice/sessions/{room}voice
POST /v1/audio/tts, POST /v1/audio/sttchat 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_id is 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), pass external_user_id explicitly to attribute the session, then query them back: GET /v1/chats?external_user_id=user_42.
  • An eut_ token's GET /v1/chats is always scoped to its own external_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_in short (default 1h) and re-mint as needed.
  • The external_id is 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.

ShareX / TwitterLinkedIn

Ready to ship?

Spin up your first agent in 5 minutes — $5 free credit.

Build your voice agent in 10 minutes

No code. No credit card. Just a phone number.

Get Started FreeTalk to Sales
Try the voice agent liveLive Demo