Authentication
Every route in this section (except GET /health) requires an API key sent as a bearer token:
Authorization: Bearer <API_KEY>
Getting a key
API keys are issued from the dashboard, shown once at creation. Store the value as soon as it's shown — there's no way to view it again later, only to revoke it and issue a new one.
A key looks like ak_xxxxxxxxxxxx....
Key scopes
Every key is minted with a scope, chosen when you create it in the dashboard:
| Scope | Bound to | Drives |
|---|---|---|
sdk (default) | A single project | The mobile agent surface (/agent/* — sessions, messages, actions). This is the key your app embeds. |
publish | A single project | The appilots CLI (/cli/* — syncing the MCP document) and issuing end-user tokens from your backend. Private — never ship it inside an app. |
operator | The whole workspace (no project) | The escalation operator surface (/escalations/*). Lets your OWN systems — a helpdesk bridge, a Slack bot, an internal support panel — act as a human operator instead of only the Appilots dashboard. See Operator API. |
The scopes are mutually exclusive by design: each key is accepted only by its own surface, even though all of them are ak_... bearer tokens sent the same way. An sdk key cannot run the CLI, an operator key is rejected by every /agent/* route, and so on. Never share a key across use cases — mint a separate key per integration.
Example
curl https://api.appilots.com/api/v1/agent/session \
-X POST \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{}'
const res = await fetch('https://api.appilots.com/api/v1/agent/session', {
method: 'POST',
headers: {
Authorization: 'Bearer ak_xxxxxxxxxxxx...',
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
Conversation capabilities (session token)
The API key identifies your application, not a conversation. Knowing a session, action, or escalation id is not enough to read or write it: every request on an existing conversation must also carry that conversation's session token.
- Create the conversation with
POST /agent/session. The response carriessessionIdandsessionToken. - Send the token on every later request about that conversation:
X-Appilots-Session-Token: <sessionToken>
This applies to everything under /agent/message, /agent/message/stream, /agent/continue, /agent/session/:sessionId/*, /agent/action/:actionId/*, and /agent/escalation*. Those message, continue, and escalation routes no longer create a session implicitly — a request without sessionId returns 422.
The session token is a signed bearer secret, valid for the lifetime of the session. It cannot be retrieved again or re-issued for an existing session, so store it alongside the sessionId. Treat it as a credential: don't log it, and clear it on logout. The official SDKs handle all of this for you.
Verified end users (optional)
A shared SDK key proves which app is calling, not which person is logged in. To attribute conversations to a real user — for identify, cross-device history, or external integrations that act on a user's behalf — your backend issues a short-lived end-user token after checking its own login session:
// Server only. Validate your app's own login session first.
const user = await requireLoggedInUser(request);
const res = await fetch('https://api.appilots.com/api/v1/cli/end-user-token', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.APPILOTS_PUBLISH_KEY}`, // scope: publish
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId: user.id, integrationGrants: [] }),
});
const { data } = await res.json(); // { token, expiresIn: 300 }
// Return data.token only to this logged-in user, with Cache-Control: no-store.
| Field | Type | Required | Description |
|---|---|---|---|
userId | string (1–128 chars) | Yes | Your stable, unique id for the user. Take it from your own session — never from client input. |
integrationGrants | array (max 16) | No | Exact tool calls this user may run through an external integration: { toolId, arguments }. The arguments must match exactly; there are no wildcards. Defaults to none. |
The client then sends the token on its requests:
X-Appilots-User-Token: <token>
Tokens last five minutes, so fetch a fresh one when it expires — the SDKs do this through their getUserToken option. Once a user token is present, any userId in a request body must match it, otherwise the request returns 403. An invalid or expired token returns 401; it never silently falls back to anonymous.
401 responses
A 401 means one of the credentials on the request was rejected:
- a missing or malformed
Authorizationheader (anything that doesn't start withBearer ak_), an invalid or expired key, or a key of the wrong scope for the route; - a missing or invalid
X-Appilots-Session-Tokenon a request about an existing conversation; - an invalid or expired
X-Appilots-User-Token.
Treat any 401 as a signal to fix the credential rather than retry the request as-is. The message field says which one was rejected. See Errors for the full error code reference.
Keeping keys safe
sdkkeys ship inside your app, so treat them as semi-public. They can start new conversations, but they can't open existing ones without that conversation's session token, and they can't read the user registry.publishandoperatorkeys are private server-side credentials. Never put them in an app bundle, SDK configuration, or client-side code.
See Security best practices for more on this.