Which credential you hold decides which parameters you may send. This is the single most common source of surprise in the interaction API: the same mint route accepts a rich body from a secret key and refuses most of it from a browser-held key — deliberately.
| Kind | Header | Where it lives | Mint scope |
|---|---|---|---|
| Secret key | Authorization: Bearer avatar_… |
Your server only | sessions:write |
| Publishable key | Authorization: Bearer pk_… + Origin |
Browser bundles | session:connect |
| Device key | Authorization: Bearer … |
Mobile apps (App Attest) | session:connect |
Trusted-issuer id_token |
Authorization: Bearer <jwt> + workspace_id in body |
Your end users’ IdP | session:connect |
| Browser session cookie | cookie, via the dashboard BFF | app.casola.ai only |
first-party route |
Create secret and publishable keys with POST /api/v1/tokens (scope tokens:write) or from the
dashboard. kind defaults to secret.
A secret key is the only credential that can select resources and rewrite behaviour, because both are server-side decisions. A publishable key ships to browsers where anyone can read it; letting it choose which prompt runs would let anyone choose which prompt runs.
| Mint parameter | Secret | Publishable / device / issuer |
|---|---|---|
persona (stock avatar) |
✅ | ✅ |
avatar_id / avatar_ref |
✅ | ❌ 403 custom_avatar_not_allowed |
avatar_version_id |
✅ | ❌ 403 avatar_version_pin_not_allowed |
profile_id |
✅ | ❌ 403 profile_pin_not_allowed |
context.system_prompt / context.backstory |
✅ | ❌ 403 tools_context_not_allowed |
extra_system_prompt / extra_backstory |
✅ | ❌ 403 tools_context_not_allowed |
tools / extra_tools |
✅ | ❌ 403 tools_context_not_allowed |
config |
✅ | ❌ 403 tools_context_not_allowed |
session_cap_seconds |
✅ | ❌ 403 session_cap_not_allowed |
response_language |
✅ | ✅ |
observability |
✅ | ✅ |
domain_allowlist |
✅ | locked to the key’s registered origins |
The refusals are explicit rather than silent. A caller that sends tools and gets a session back
without them would find out in production; a 403 is found in development.
browser ──▶ your backend (holds the secret key) ──▶ POST /api/v1/sessions
▲ │
└────────── connect_url + session_token ◀──────────────┘
│
└──▶ direct WebSocket to the GPU edge
Your backend is where you decide who gets which avatar with what overrides — the decisions a
browser cannot be trusted with. The session_token it returns is safe to hand to the browser: it
is scoped to one session, expires in 60 seconds, and grants nothing but the right to connect.
// your-backend/start-session.ts
app.post('/start-session', async (req, res) => {
const user = await authenticateYourUser(req); // your auth, not ours
const mint = await fetch('https://api.casola.ai/api/v1/sessions', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.CASOLA_SECRET_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({
avatar_id: pickAvatarFor(user),
// Per-session context — this user only, never stored:
extra_system_prompt: `The caller is ${user.name}, a ${user.plan} customer.`,
}),
});
const { connect_url, session_token } = await mint.json();
res.json({ connect_url, session_token }); // the only two fields the browser needs
});
Never return seat_token to a browser you do not control — it is the capacity handle, and it lets
the holder release the seat.
If you have no server, a publishable key can mint directly from the browser, restricted to stock avatars and no behaviour overrides:
const r = await fetch('https://api.casola.ai/api/v1/sessions', {
method: 'POST',
headers: { authorization: `Bearer ${PUBLISHABLE_KEY}`, 'content-type': 'application/json' },
body: JSON.stringify({ persona: 'mei' }),
});
The request Origin must match one of the origins registered on the key, and domain_allowlist is
forced to those origins regardless of what you send.
POST /api/sessions is the dashboard’s own mint, authenticated by browser session cookie rather
than a key. It exists for the “chat with this avatar” button inside app.casola.ai. It accepts
persona, avatar_id / avatar_ref, profile_id, response_language, observability, and the
two additive text extras — but not tools, extra_tools, context, or config. A caller
sending extra_tools gets 400 extra_tools_not_allowed rather than a silent drop.
It is not part of the public integration surface; use POST /api/v1/sessions from your own
backend.
| Scope | Grants |
|---|---|
sessions:write |
Mint sessions (POST /api/v1/sessions) |
sessions:read |
GET /api/v1/sessions/:id |
avatars:read |
List/read avatars, versions, profiles |
avatars:write |
Create/update avatars, publish versions, append profiles |
session:connect |
Mint from a browser-held credential (publishable/device/issuer) |
Avatar authoring additionally requires the custom_avatars workspace feature. Reading, renaming,
deleting and opening sessions stay available if that feature is ever revoked — you keep using and
managing what you already built.
Next: 2. Choosing an avatar →