Skip to main content

Webhooks

Webhooks let your backend receive a server-to-server callback whenever specific events happen in your project — no polling required.

Creating a subscription

  1. Open your project in the dashboard.
  2. Go to Webhooks.
  3. Add a new subscription with:
    • A destination URL (must be http:// or https://).
    • The events you want to receive.
  4. Save. Appilots starts sending a POST request to your URL whenever a subscribed event occurs.

Available events

EventFires when
project.createdA project is created.
project.deletedA project is deleted.
conversation.escalatedA human escalation is created (a user requests a human, or the agent gives up and the user accepts the handoff).
escalation.messageA message crosses the escalation channel, in either direction — the end-user or the operator.
escalation.claimedAn operator claims a pending escalation (from the dashboard or via an operator API key).
escalation.resolvedAn escalation is resolved and control returns to the AI.

This is the complete list of currently available events — more may be added over time.

Escalation event payloads

The four escalation events share a payload shape (data field of the delivery body):

FieldTypePresent onDescription
escalationIdstringallThe escalation this event belongs to.
sessionIdstringallThe underlying agent session.
projectIdstringallThe project the session belongs to.
reasonstringconversation.escalateduser_requested or agent_gave_up.
statusstringconversation.escalated, escalation.claimed, escalation.resolvedThe escalation's status after this event.
operatorNamestringescalation.claimed, escalation.message (operator replies)Display name of the operator — a dashboard user's name, or the operatorName an operator API key attached to the claim/reply.
messageobjectescalation.message{ id, role, content, createdAt }role is user or human_agent.

Example escalation.claimed delivery:

{
"event": "escalation.claimed",
"workspaceId": "...",
"data": {
"escalationId": "esc_...",
"sessionId": "sess_...",
"projectId": "proj_...",
"status": "active",
"operatorName": "Zendesk Bridge"
},
"timestamp": "2026-07-18T12:00:00.000Z"
}

Example escalation.message delivery (an operator reply):

{
"event": "escalation.message",
"workspaceId": "...",
"data": {
"escalationId": "esc_...",
"sessionId": "sess_...",
"projectId": "proj_...",
"operatorName": "Zendesk Bridge",
"message": {
"id": "msg_...",
"role": "human_agent",
"content": "Hi, this is support — how can I help?",
"createdAt": "2026-07-18T12:00:01.000Z"
}
},
"timestamp": "2026-07-18T12:00:01.000Z"
}

Delivery format

Each delivery is a POST request with a JSON body shaped like this:

{
"event": "project.created",
"workspaceId": "...",
"data": {
// event-specific fields
},
"timestamp": "2026-07-18T12:00:00.000Z"
}

Two headers accompany every delivery:

HeaderPurpose
X-Appilots-EventThe event name, same as the event field in the body — a convenience for routing without parsing the body first.
X-Appilots-Signaturesha256=<hex-hmac> — an HMAC-SHA256 signature of the raw request body, described below.

Verifying the signature

When you create a webhook subscription, Appilots generates a secret for it and shows it to you once, at creation time — copy it immediately, the same way you would an API key. There's no way to view it again after that; if you lose it, you'll need to create a new subscription.

To verify a delivery is genuinely from Appilots, recompute the HMAC-SHA256 of the raw request body using your stored secret, and compare it against the sha256= value in the X-Appilots-Signature header. Reject the delivery if they don't match.

const crypto = require('crypto');

function isValidSignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');

const received = signatureHeader.replace(/^sha256=/, '');

return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex')
);
}

Compute the HMAC over the exact raw bytes of the request body — if your framework parses the body into an object before you can access the raw bytes, configure it to preserve the raw payload for this check. Use a constant-time comparison (like crypto.timingSafeEqual) rather than === to avoid timing attacks.

Delivery failures

A delivery is considered failed if your endpoint doesn't respond successfully within a short timeout, or responds with a non-2xx status. Failed deliveries are logged and visible in the dashboard for that webhook, so you can check delivery history if events aren't showing up where you expect.

Respond with a 2xx status as quickly as possible, and do any slow processing asynchronously after acknowledging receipt — don't make Appilots wait on your downstream work before you respond.

Next steps