Pular para o conteúdo principal

External Support Integrations

Appilots' human-escalation queue doesn't have to live only in the Appilots dashboard. With an operator API key, your own systems — a Zendesk/Freshdesk bridge, a Slack bot, an internal support panel — can claim, reply to, and resolve escalations directly. This guide walks through building that bridge: a webhook in (Appilots tells you something needs attention) and a reply API out (you tell Appilots what the operator said).

This is a different integration than the SDK's built-in escalation UI, which is the end-user (client) side. This guide is entirely about the operator side.

Why build a bridge

  • Your support team already lives in Slack, Zendesk, or a custom tool — don't make them context-switch to a second dashboard.
  • Route escalations into your existing on-call/ticketing workflow (priority, assignment, SLAs) instead of a flat queue.
  • Keep a single system of record for every support conversation, in-app or otherwise.

1. Mint an operator API key

In the dashboard, go to Workspace → API keys → Gerar chave, and choose Operador (atendimento) as the type. Unlike an SDK key, this one:

  • is workspace-wide (not bound to a single project) — it can work escalations across every project in the workspace;
  • is rejected by the SDK surface (/agent/*) — a leak can't be used to impersonate the mobile agent;
  • is accepted by every /escalations/* route the dashboard itself uses.

Store it like any other secret — it's shown once. See Authentication → Key scopes for the full comparison against SDK keys.

2. Wire the webhook in

Subscribe a URL on your bridge to the escalation lifecycle events (see Dashboard: Webhooks):

EventUse it to
conversation.escalatedOpen a ticket / post a new Slack thread.
escalation.messageRelay the end-user's message into that ticket/thread.
escalation.claimedReflect who's handling it (useful if claimed from the dashboard by a human, so your bridge doesn't try to double-claim).
escalation.resolvedClose the ticket / archive the thread.

Every delivery is HMAC-signed — verify it before acting on the payload (see Webhooks → Verifying the signature). Respond 2xx immediately and do your Slack/Zendesk calls asynchronously.

// Sketch of a webhook receiver — verify, then react by event type.
app.post('/appilots/webhook', (req, res) => {
if (!isValidSignature(req.rawBody, req.header('X-Appilots-Signature'), WEBHOOK_SECRET)) {
return res.status(401).end();
}
res.status(200).end(); // ack first, work after

const { event, data } = req.body;
switch (event) {
case 'conversation.escalated':
openSlackThread(data.escalationId, data.sessionId, data.reason);
break;
case 'escalation.message':
if (data.message.role === 'user') relayToSlack(data.escalationId, data.message.content);
break;
case 'escalation.resolved':
archiveSlackThread(data.escalationId);
break;
}
});

3. Reply out through the operator API

When a human on your side (in Slack, in Zendesk) types a reply, send it back with the operator key. A reply to a pending escalation auto-claims it, so a single call is often enough:

async function replyAsOperator(escalationId: string, content: string, operatorName: string) {
const res = await fetch(
`https://api.appilots.com/api/v1/escalations/${escalationId}/message`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.APPILOTS_OPERATOR_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ content, operatorName }),
},
);
return res.json();
}

Pass operatorName (the human agent's name, e.g. their Slack display name) on every claim/reply — it surfaces in the Appilots dashboard as claimedByName, so a team that mixes dashboard operators and bridge operators still sees who's who. See API reference: Operator API → Attribution for the full field.

When the human on your side marks the ticket/thread resolved, call resolve:

async function resolveEscalation(escalationId: string) {
await fetch(`https://api.appilots.com/api/v1/escalations/${escalationId}/resolve`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.APPILOTS_OPERATOR_KEY}` },
});
}

A minimal Slack-bridge sketch

Putting it together — a bridge that opens a thread per escalation and relays messages both ways:

// Webhook in (Appilots → bridge)
app.post('/appilots/webhook', async (req, res) => {
if (!isValidSignature(req.rawBody, req.header('X-Appilots-Signature'), WEBHOOK_SECRET)) {
return res.status(401).end();
}
res.status(200).end();

const { event, data } = req.body;
if (event === 'conversation.escalated') {
const thread = await slack.chat.postMessage({
channel: SUPPORT_CHANNEL,
text: `New escalation (${data.reason}) — session ${data.sessionId}`,
});
await db.threads.insert({ escalationId: data.escalationId, ts: thread.ts });
}
if (event === 'escalation.message' && data.message.role === 'user') {
const thread = await db.threads.findByEscalation(data.escalationId);
await slack.chat.postMessage({
channel: SUPPORT_CHANNEL,
thread_ts: thread.ts,
text: `User: ${data.message.content}`,
});
}
});

// Reply out (bridge → Appilots), triggered by a Slack message event
// in a thread the bridge is tracking:
slackEvents.on('message', async (event) => {
const escalation = await db.threads.findByTs(event.thread_ts);
if (!escalation || event.bot_id) return;
await replyAsOperator(escalation.escalationId, event.text, event.user_display_name);
});

// A `/resolve` Slack slash command in the thread:
slackCommands.on('/resolve', async (command) => {
const escalation = await db.threads.findByTs(command.thread_ts);
if (escalation) await resolveEscalation(escalation.escalationId);
});

This is intentionally minimal — production bridges usually add retry/backoff on the outbound calls, idempotency on the inbound webhook (dedupe by escalationId + event type + timestamp), and a reconciliation sweep against GET /escalations?status=open in case a webhook delivery is ever missed.

Next steps