Escalations
An escalation hands a conversation off from the agent to a human operator. This page covers the client-side API for starting one, sending messages into it, and following the operator's replies. For the SDK's built-in UI for this flow, see SDK: Escalation. For the operator side (queues, replying, resolving), see Dashboard: Human escalation ops.
Lifecycle
- Start an escalation with
POST /agent/escalation. - Exchange messages — the user sends messages with
POST /agent/escalation/:id/message; you receive operator replies either by pollingGET /agent/escalation/:idor by subscribing toGET /agent/escalation/:id/stream. - End the escalation — either the operator resolves it, or your client calls
POST /agent/escalation/:id/abandonif the user leaves the conversation (e.g. clears the chat) so an operator isn't left waiting.
Start an escalation
POST /agent/escalation
Starts a new human handoff, or resumes one if the session already has an open (non-resolved) escalation — this endpoint is idempotent, so calling it again on a session with a pending or active escalation returns the existing one instead of creating a duplicate.
Auth: required.
Body
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | No | Session to escalate. If omitted, a new session is created. |
reason | 'user_requested' | 'agent_gave_up' | Yes | Why the escalation is happening. |
note | string | No | Additional context for the operator. |
Response — 201 (new) or 200 (resumed)
{
"success": true,
"data": {
"escalationId": "...",
"sessionId": "...",
"status": "pending"
}
}
curl
curl https://api.appilots.com/api/v1/agent/escalation \
-X POST \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{"sessionId": "SESSION_ID", "reason": "user_requested"}'
TypeScript
const res = await fetch("https://api.appilots.com/api/v1/agent/escalation", {
method: "POST",
headers: {
Authorization: "Bearer ak_xxxxxxxxxxxx...",
"Content-Type": "application/json",
},
body: JSON.stringify({ sessionId, reason: "user_requested" }),
});
const { data } = await res.json();
Send a message into an escalation
POST /agent/escalation/:id/message
Routes a user message to the human operator, not the AI.
Auth: required.
Body
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | The user's message text. |
Response — 201
{
"success": true,
"data": { "messageId": "..." }
}
A request that fails body validation returns 422 (VALIDATION_ERROR). An unknown escalation id returns 404 (NOT_FOUND). A message sent to an already-resolved escalation returns 409 (ESCALATION_RESOLVED).
curl
curl https://api.appilots.com/api/v1/agent/escalation/ESCALATION_ID/message \
-X POST \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{"content": "I still need help with this"}'
TypeScript
const res = await fetch(
`https://api.appilots.com/api/v1/agent/escalation/${escalationId}/message`,
{
method: "POST",
headers: {
Authorization: "Bearer ak_xxxxxxxxxxxx...",
"Content-Type": "application/json",
},
body: JSON.stringify({ content: "I still need help with this" }),
}
);
const { data } = await res.json();
Poll for status and operator replies
GET /agent/escalation/:id?since=<ISO timestamp>
Auth: required.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
since | string (ISO timestamp) | No | Only return operator messages created after this time. Omit for the full history. |
Response — 200
{
"success": true,
"data": {
"escalationId": "...",
"sessionId": "...",
"status": "active",
"messages": [
{
"id": "...",
"role": "human_agent",
"content": "...",
"createdAt": "2026-01-01T00:00:00.000Z"
}
]
}
}
An unknown escalation id returns 404 (NOT_FOUND).
curl
curl "https://api.appilots.com/api/v1/agent/escalation/ESCALATION_ID?since=2026-01-01T00:00:00.000Z" \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..."
TypeScript
const res = await fetch(
`https://api.appilots.com/api/v1/agent/escalation/${escalationId}?since=${lastSeen}`,
{ headers: { Authorization: "Bearer ak_xxxxxxxxxxxx..." } }
);
const { data } = await res.json();
Stream operator replies live
GET /agent/escalation/:id/stream?since=<ISO timestamp>
An SSE channel mirroring the poll endpoint, pushed in real time. Use since after a reconnect to catch up on anything missed.
Auth: required.
Events
type | Fires | Payload |
|---|---|---|
status | Once immediately with current status, then again on any change | { "type": "status", "status": "pending" | "active" | "resolved", "operatorName"?: "..." } |
human-message | Once per operator message, backlog first then live | { "type": "human-message", "message": { "id", "content", "createdAt" } } |
An unknown escalation id returns 404 as a plain JSON error body (not SSE).
The connection is kept alive with periodic pings. Reconnecting after a drop is the client's responsibility — there's no server-side auto-reconnect. When you reconnect, pass since set to the createdAt of the last message you received, so you don't lose anything in between.
curl
curl "https://api.appilots.com/api/v1/agent/escalation/ESCALATION_ID/stream?since=2026-01-01T00:00:00.000Z" \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..." \
-N
TypeScript
const res = await fetch(
`https://api.appilots.com/api/v1/agent/escalation/${escalationId}/stream?since=${lastSeen}`,
{ headers: { Authorization: "Bearer ak_xxxxxxxxxxxx..." } }
);
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
for (const line of buffer.split("\n\n")) {
if (!line.startsWith("data: ")) continue;
const event = JSON.parse(line.slice(6));
// handle event.type: "status" | "human-message"
}
}
Abandon an escalation
POST /agent/escalation/:id/abandon
Tells the server the user left the conversation, so the escalation is closed instead of leaving an operator waiting.
Auth: required.
Response — 200
{
"success": true,
"data": { "status": "resolved" }
}
This is idempotent — calling it on an already-resolved escalation also returns 200 with status: "resolved" as a no-op. An unknown escalation id returns 404 (NOT_FOUND).
curl
curl https://api.appilots.com/api/v1/agent/escalation/ESCALATION_ID/abandon \
-X POST \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..."
TypeScript
const res = await fetch(
`https://api.appilots.com/api/v1/agent/escalation/${escalationId}/abandon`,
{ method: "POST", headers: { Authorization: "Bearer ak_xxxxxxxxxxxx..." } }
);
const { data } = await res.json();
Operator API (server-to-server)
Everything above is the client (end-user) side of an escalation. The operator side — claiming, replying, resolving — normally happens in the Appilots dashboard, but you can drive it from your OWN systems instead: a helpdesk bridge, a Slack bot, an internal support panel. This section covers that surface.
Auth
Operator routes accept either:
- the dashboard JWT +
X-Workspace-Id(what the dashboard itself uses), or - a workspace-scoped operator API key —
Authorization: Bearer ak_xxxxxxxxxxxx...where the key's scope isoperator(see Authentication → Key scopes).
Mint an operator key from Workspace → API keys, choosing "Operador (atendimento)" as the type. Unlike an sdk key, an operator key isn't bound to a single project — it works the queue across every project in the workspace, and it is rejected by the SDK surface (/agent/*), so a leaked operator key can't be used to impersonate the mobile agent.
curl https://api.appilots.com/api/v1/escalations \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..."
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /escalations?status=open|resolved | List the queue (open = pending + active, default). |
GET | /escalations/:id | Detail + full transcript for one escalation. |
GET | /escalations/stream | SSE feed of every escalation event in the workspace. |
POST | /escalations/:id/claim | Claim a pending escalation. |
POST | /escalations/:id/message | Reply as the operator. |
POST | /escalations/:id/resolve | Resolve, handing the conversation back to the AI. |
These are the same routes the dashboard itself calls — an operator key just authenticates differently. Response shapes, status codes, and state transitions (pending → active → resolved) are identical to the dashboard flow described in Human escalation ops.
Attribution
Key-authenticated operators aren't a users row in the dashboard, so claim and reply both accept an optional operatorName — a display name for whoever (or whatever system) is acting on the other end. It shows up as claimedByName in the queue and detail responses, and as the operatorName on the corresponding webhook/SSE events.
Claim
POST /escalations/:id/claim
| Field | Type | Required | Description |
|---|---|---|---|
operatorName | string (1–120 chars) | No | Display name for this operator. Falls back to the API key's own name if omitted. |
curl https://api.appilots.com/api/v1/escalations/ESCALATION_ID/claim \
-X POST \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{"operatorName": "Zendesk Bridge"}'
Reply
POST /escalations/:id/message
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | The operator's reply text. |
operatorName | string (1–120 chars) | No | Same as above — set independently per reply if different agents on your side handle the same thread. |
curl https://api.appilots.com/api/v1/escalations/ESCALATION_ID/message \
-X POST \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{"content": "Hi, this is support — how can I help?", "operatorName": "Zendesk Bridge"}'
A reply to a pending escalation auto-claims it, same as the dashboard — you don't need a separate claim call if you're going straight to a reply.
Resolve
POST /escalations/:id/resolve
No body. Same response shape as the dashboard resolve.
curl https://api.appilots.com/api/v1/escalations/ESCALATION_ID/resolve \
-X POST \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..."
Building a bridge
For a worked example wiring an external system's inbound webhook to this API's outbound calls (e.g. a Slack bot), see Guides: External support integrations.
Related pages
- SDK: Escalation — the client-side SDK component/hooks for this flow
- Dashboard: Human escalation ops — the operator-facing side
- Dashboard: Webhooks — subscribe to the escalation lifecycle events
- Guides: External support integrations — building a bridge with the operator API
- Errors — full error code reference