Pular para o conteúdo principal

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

  1. Start an escalation with POST /agent/escalation.
  2. Exchange messages — the user sends messages with POST /agent/escalation/:id/message; you receive operator replies either by polling GET /agent/escalation/:id or by subscribing to GET /agent/escalation/:id/stream.
  3. End the escalation — either the operator resolves it, or your client calls POST /agent/escalation/:id/abandon if 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

FieldTypeRequiredDescription
sessionIdstringNoSession to escalate. If omitted, a new session is created.
reason'user_requested' | 'agent_gave_up'YesWhy the escalation is happening.
notestringNoAdditional 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

FieldTypeRequiredDescription
contentstringYesThe 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

ParamTypeRequiredDescription
sincestring (ISO timestamp)NoOnly 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

typeFiresPayload
statusOnce immediately with current status, then again on any change{ "type": "status", "status": "pending" | "active" | "resolved", "operatorName"?: "..." }
human-messageOnce 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 keyAuthorization: Bearer ak_xxxxxxxxxxxx... where the key's scope is operator (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

MethodPathPurpose
GET/escalations?status=open|resolvedList the queue (open = pending + active, default).
GET/escalations/:idDetail + full transcript for one escalation.
GET/escalations/streamSSE feed of every escalation event in the workspace.
POST/escalations/:id/claimClaim a pending escalation.
POST/escalations/:id/messageReply as the operator.
POST/escalations/:id/resolveResolve, 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
FieldTypeRequiredDescription
operatorNamestring (1–120 chars)NoDisplay 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
FieldTypeRequiredDescription
contentstringYesThe operator's reply text.
operatorNamestring (1–120 chars)NoSame 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.