Workspace webhooks reference

Events, payloads, headers and signature verification for workspace webhooks.

A workspace webhook tells one of your own endpoints when something happens to a conversation. You add endpoints in the dashboard's workspace settings, on the webhooks page. Each endpoint is one of four kinds and subscribes to the events you choose.

KindWhat it receives
DiscordA formatted card posted through a Discord webhook URL
SlackA formatted message posted through a Slack incoming webhook URL
TelegramA formatted message sent by your bot to a chat
CustomA signed JSON body sent to your HTTPS endpoint

The rest of this page describes the Custom kind, which is the one meant for code.

Events

EventSent when
conversation.createdA visitor starts a conversation, or answers a proactive message
message.receivedA visitor sends a message
conversation.escalatedAiva hands the conversation to your team
conversation.assignedA teammate becomes responsible for the conversation
conversation.unassignedThe conversation no longer has a responsible teammate
handoff.overdueNobody picked up a handoff within the response target you set for the team. Never sent for a team without one
conversation.transferredA teammate transferred the conversation to another team
conversation.resolvedThe conversation is resolved
conversation.closedThe conversation is closed

An endpoint subscribed to conversation.assigned before conversation.unassigned existed receives both. conversation.transferred reaches only the endpoints subscribed to it; no endpoint is subscribed to it for you. The dashboard offers it once your workspace has more than one team.

When a team assigns automatically, a conversation handed to it goes to one member who has the dashboard open, and that assignment is a conversation.assigned with no assignerName. So is a move to another member when the first one leaves before replying, or when nobody picks it up within the team's response target. When the conversation goes back to the whole team instead, it is a conversation.unassigned with no assignerName.

Body

Every delivery has the same envelope:

{
  "event": "conversation.assigned",
  "timestamp": "2026-09-28T14:02:11.000Z",
  "data": {
    "conversationId": "c2779aa20b318-476a1184e6fc2",
    "contactName": "Ada",
    "agentName": "Grace",
    "assignerName": "Alan",
    "team": { "id": "cm1team8x0001", "name": "Support" },
    "assignee": { "id": "cm1mbr4k20007", "name": "Grace" }
  },
  "links": {
    "conversation": "https://dash.assistify.chat/open/…"
  }
}

Fields in data, by event:

FieldEventsMeaning
conversationIdallThe conversation's id
contactNameallThe visitor's name, or a fallback when they have not given one
messagePreviewmessage.receivedThe first 300 characters of the message
reasonconversation.escalatedWhy Aiva handed the conversation over
summaryconversation.escalatedAiva's summary for the team
isUrgentconversation.escalatedWhether the request was marked urgent
agentNameconversation.assignedThe teammate now responsible
assignerNameconversation.assigned, conversation.unassigned, conversation.transferredThe teammate who made the change, absent when the product made it
teamall, once a team holds the conversationThe team that holds the conversation: id and name
assigneeall, while a teammate is responsibleThe teammate responsible for the conversation: id and name
handoffconversation.escalated, handoff.overdue, conversation.transferredThe handoff: its id, and dueAt, when the team's response target passes. For a transfer, the receiving team's handoff
reassignedhandoff.overdueWhether the handoff went to another teammate, who then holds it; when false, the team's lead has been told
fromTeamconversation.transferredThe team the conversation left: id and name. Absent when it had none
toTeamconversation.transferredThe team the conversation reached: id and name. team names it too
noteconversation.transferredThe note the teammate left for the receiving team, when there is one

team and assignee describe the conversation when the event is sent. An assignee id identifies a member of your workspace; the same person has a different id in another workspace.

reason is one of user_requested, cannot_answer, frustrated_user, requires_authorization, unresolved_attempts, sensitive_topic, qualified_lead, wrong_scope or ai_unavailable.

Fields are only ever added. Ignore fields you do not recognize.

Headers

HeaderValue
X-Assistify-EventThe event name, the same as event in the body
X-Assistify-TimestampUnix seconds at signing
X-Assistify-Signaturesha256= followed by the hex HMAC-SHA256 of <timestamp>.<raw body>
X-Assistify-Delivery-IdA UUID that stays the same across retries of one delivery

Verifying the signature

Compute the HMAC over the timestamp, a dot and the raw request body, with the endpoint's signing secret from the dashboard. Compare it with the header in constant time, and reject a timestamp that is too old for your needs.

import { createHmac, timingSafeEqual } from 'node:crypto';
 
function isFromAssistify(rawBody, headers, secret) {
  const timestamp = headers['x-assistify-timestamp'];
  const expected = 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const received = headers['x-assistify-signature'] ?? '';
  return expected.length === received.length && timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}

Retries

A delivery that does not get a 2xx response is retried up to five attempts in all, with an exponential backoff starting at two seconds. Every attempt carries the same X-Assistify-Delivery-Id, so you can ignore one you have already processed.

Test delivery

The test button on an endpoint sends the event test.ping, in the header and in the body:

{ "event": "test.ping", "timestamp": "…", "data": { "message": "This endpoint receives Assistify events." } }