End-user credentials
Ship apps whose users never see an API key — mint short-lived, per-user JWTs from your server.
Two roles hold credentials, and they must never swap:
| Role | Credential | Lives |
|---|---|---|
| You (the developer) | workspace API key, cosmo_… | your server, your laptop — never a shipped binary or a browser bundle |
| Your end user | minted JWT, eyJ… | their device, short-lived, scoped to one user |
An API key in a shipped app is your whole realtime workspace in every user's hands. The minting flow keeps the key server-side and hands each user a token that can join sessions but nothing else.
The flow
end-user app ──"log me in"──► your server ──POST /auth/token──► Cosmo
▲ │ (API key, user_tokens:mint)
└───────── JWT ◄──────────────┘1. Your server mints (holds the API key):
client = CosmoRealtime(api_key=os.environ["COSMO_API_KEY"])
minted = await client.mint_token(external_user_id=user.id)
# → minted.jwt, minted.expires_atconst client = new RealtimeClient({ apiKey: process.env.COSMO_API_KEY });
const { jwt, expiresAt } = await client.mintToken(user.id);external_user_id is an opaque string of your choosing (1–128 chars) — your user id, not an email. Cosmo uses it to attribute sessions and usage per user; it's the join key between your users and your usage dashboard.
2. The device connects with the JWT:
const client = new RealtimeClient({ token: jwt });
const session = await client.agent({ instructions: '…' }).start();let session = try await RealtimeSession.start(
.init(token: jwt, baseURL: URL(string: "https://app.askcosmo.ai")!),
config: config
)Provisioning keys
For defense in depth, mint with a provisioning key — an API key carrying only the user_tokens:mint scope. It can create end-user tokens but cannot join sessions itself, so even a leak of your minting endpoint's credential doesn't grant conversational access. Your session-capable key (realtime:use) stays wherever server-side agents actually run. Scopes are managed per key in the dashboard.
Expiry and refresh
Minted tokens are short-lived; expires_at tells you when. The practical pattern:
- Mint at app launch / login, and re-mint when
expires_atapproaches — treat it like any OAuth-ish access token. - A token only needs to be valid at session start; an in-flight session is not cut off by its token expiring.
- On a
401/auth error at session start, re-mint and retry once before surfacing an error.
What tokens can and cannot do
API key (realtime:use) | Minted JWT | |
|---|---|---|
| Start sessions | ✓ | ✓ |
| Client tools, hooks, skills | ✓ | ✓ |
| Catalog agents | ✓ | ✓ |
| Dial phone numbers | ✓ | ✗ |
| Transfer-call tools | ✓ | ✗ (server-tool references only) |
| Mint tokens | with user_tokens:mint | ✗ |
The boundary is intentional: anything that spends money on the phone network or touches workspace routing stays behind the key you control.