RaabtaHQ
Webhooks

Webhooks

Subscribe to events from RaabtaHQ: the event catalogue, the envelope, HMAC signature verification in Node and Python, the retry schedule and delivery rules.

Events

A webhook subscription names an HTTPS URL and the event types it wants. Each event is delivered as its own HTTP POST. Subscriptions are managed from the dashboard and through the API with the webhooks:manage scope.

EventResourceFires when
message.receivedmessageA customer sent a message on any channel. Delivered immediately from the inbound path, alongside the assistant dispatch.
message.sentmessageA message was sent to a customer by a member, the assistant or the API. Internal AI-activity notes are never sent.
message.status_updatedmessageThe channel reported a delivery status change for an outbound message (sent → delivered → read, or failed). Queued and delivered by the next drain pass.
conversation.createdconversationA new thread was opened with a contact on a channel.
conversation.status_changedconversationThe thread moved between open, pending and closed.
conversation.assignedconversationThe thread was assigned to a member, reassigned, or unassigned (`assignee_user_id: null`).
conversation.handoffconversationThe assistant (or a member) flagged the thread as needing a human. `source` says who; `reason` is what they recorded.
contact.createdcontactA contact was created — from an inbound message, the dashboard, a CSV import or the API.
contact.updatedcontactName, phone, email, company or avatar changed. Edits in the same minute are collapsed into one event; tag changes have their own events.
contact.tag_addedcontactA tag was applied to a contact.
contact.tag_removedcontactA tag was removed from a contact.
deal.createddealA deal was added to a pipeline.
deal.stage_changeddealThe deal moved between stages. Every move is reported, including moving back to a stage it has already been in.
deal.wondealThe deal was closed as won.
deal.lostdealThe deal was closed as lost.
broadcast.completedbroadcastA broadcast finished sending (`status: sent`) or stopped (`status: failed`). The counts are final at this point.
booking.createdbookingA booking was made — by staff, by the assistant, or from the public booking page.
booking.cancelledbookingA booking was cancelled. `cancel_reason` carries what was recorded.
booking.completedbookingA booking was marked completed.
inquiry.createdinquirySomeone wrote in through a website contact form or the API. Delivered immediately from the intake path, not on the next sweep, because a lead that arrives ten minutes late has usually gone to whoever answered first. A submission from a blocked sender is never recorded and never delivered.
inquiry.status_changedinquiryThe inquiry moved between new, in progress, closed and spam — from the dashboard, the API or the assistant. Every move is reported, including moving back to a state it has already been in. Queued and delivered by the next drain pass.
assistant.run_completedrunAn assistant finished what it was asked to do. `result.outcome` says how it went and `result.summary` says what happened in a line. A sub-agent does not fire this — the parent run reports for the whole tree.
assistant.run_failedrunA run stopped without finishing — it errored, hit a guardrail, ran out of budget, or somebody cancelled it. `status` tells cancellation from failure, and `error_code` says why.
assistant.approval_requestedrunA run stopped to ask a person something, and is parked until it is answered or `expires_at` passes. It holds that conversation’s only run slot while it waits, so this is the event worth acting on quickly. Answer with POST /api/v1/asks/{id}/answer.

The envelope

Every delivery has the same outer shape; data is the event's own payload, documented per event below. api_version is 2026-09-01 today and changes only with the payload shapes.

POST body
{
  "id": "evt_01J9X4V8Q2ZK7N3M",
  "type": "message.received",
  "api_version": "2026-09-01",
  "created_at": "2026-09-02T10:00:00.000Z",
  "account_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
  "data": {
    "message": {
      "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
      "conversation_id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
      "direction": "inbound",
      "sender_type": "customer",
      "type": "text",
      "text": "Is the clinic open on Saturday?",
      "media_url": null,
      "template_name": null,
      "status": "delivered",
      "channel_message_id": "wamid.HBgLOTcxNTAxMjM0NTY3FQIAEhgUM0E5QjZDRkQ0RkQ5QjM3RTQ3NkQA",
      "reply_to_message_id": null,
      "created_at": "2026-09-02T09:58:00.000Z"
    },
    "contact": {
      "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
      "name": "Ayesha Khan",
      "phone": "+971501234567"
    },
    "conversation": {
      "id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
      "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
      "contact": {
        "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
        "name": "Ayesha Khan",
        "phone": "+971501234567"
      },
      "channel": "whatsapp",
      "connection_id": null,
      "status": "open",
      "assigned_to": null,
      "ai_ownership": null,
      "needs_attention": false,
      "handoff_reason": null,
      "last_message_text": "Is the clinic open on Saturday?",
      "last_message_at": "2026-09-02T09:58:00.000Z",
      "unread_count": 1,
      "created_at": "2026-09-01T08:15:00.000Z",
      "updated_at": "2026-09-02T09:58:00.000Z"
    }
  }
}
NameTypeDescription
idrequired
stringUnique event id — dedupe on this; delivery is at-least-once
typerequired
stringThe event type, e.g. contact.created
api_versionrequired
stringThe payload version this endpoint is pinned to
created_atrequired
string (date-time)When the event was recorded
account_idrequired
string (uuid)The account the event belongs to
datarequired
anyThe event-specific payload

Headers

HeaderMeaning
X-Raabta-Signaturet=<unix seconds>,v1=<hex>. The signature over the timestamp and the raw body — see below. During a secret rotation there may be more than one v1=.
X-Raabta-TimestampThe same timestamp as t=, on its own, as Unix seconds.
X-Raabta-EventThe event type, so a receiver can route before parsing.
X-Raabta-DeliveryA unique id for this delivery. Retries of the same event carry the same envelope id but a new delivery id.
X-Raabta-AttemptWhich attempt this is, starting at 1, up to 6.

Verifying the signature

Each subscription has a secret, shown once when it is created. The signature is sha256 over the string <timestamp>.<raw body>, hex-encoded. To verify:

  1. Read the raw request bytes. Do not parse and re-serialise — any change to whitespace or key order changes the signature.
  2. Split X-Raabta-Signature on commas; take t and every v1.
  3. Reject if |now − t| exceeds 300 seconds — that bounds how long a captured delivery can be replayed.
  4. Compute the HMAC and compare it to each v1 with a constant-time comparison. Any match is valid.
Request
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

/** rawBody must be the exact bytes received — parse JSON only after this passes. */
export function verifyWebhook(rawBody, headers, secret) {
  const header = headers["x-raabta-signature"] ?? "";
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=", 2)),
  );
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  // Every v1= value is checked so a secret rotation never drops a delivery.
  return header
    .split(",")
    .filter((kv) => kv.startsWith("v1="))
    .map((kv) => kv.slice(3))
    .some(
      (sig) =>
        sig.length === expected.length &&
        timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex")),
    );
}

Verify before anything else

A delivery that fails verification must be discarded without being acted on — and without a distinguishing response. Return the same status you would for a malformed request.

Delivery and retries

Respond with any 2xx within 10 seconds. Anything else — a 3xx, a 4xx, a 5xx, a timeout, a TLS failure — counts as a failed attempt and the delivery is retried on this schedule:

AttemptDelay after the previous attempt
21 min
35 min
430 min
52 h
612 h
  • At most 6 attempts per event. After the last one the delivery is recorded as failed and visible in the dashboard.
  • After 10 consecutive failed deliveries the subscription is disabledand the account's admins are notified. Re-enable it from the dashboard once the endpoint is healthy; events that fired while it was disabled are not delivered.
  • Deliveries are at-least-once and may arrive out of order. Dedupe on the envelope id (evt_…): a retry carries the same id.
  • Do the work after responding — queue the event and return 200 — rather than inside the 10-second window.

Endpoint URL rules

A subscription URL is validated when it is created and again before every delivery:

  • HTTPS only.
  • No credentials in the URL (user:pass@host).
  • At most 2048 characters.
  • The host must not resolve to a private, loopback, link-local or cloud-metadata address. Checked when the endpoint is created and re-resolved on every delivery.
  • Redirects are not followed — a 3xx counts as a failed delivery.

Event payloads

The data object for each event, and an example as it would be delivered.

message.received

A customer sent a message on any channel. Delivered immediately from the inbound path, alongside the assistant dispatch.

NameTypeDescription
messagerequired
object
message.idrequired
string (uuid)UUID
message.conversation_idrequired
string (uuid)UUID
message.directionrequired
string (enum)inbound = from the customerinboundoutbound
message.sender_typerequired
string (enum)bot = the assistant or an API key; agent = a member in the inboxcustomeragentbot
message.typerequired
string (enum)Content typetextimagedocumentaudiovideolocationtemplateinteractive
message.textrequired
string | nullBody text, or the caption of a media message
message.media_urlrequired
string | nullURL of the attached image, video, document or audio
message.template_namerequired
string | nullThe template this message was sent from, for template sends
message.statusrequired
string (enum)Delivery status as reported by the channelsendingsentdeliveredreadfailed
message.channel_message_idrequired
string | nullThe channel’s own id (a WhatsApp wamid, a Messenger mid)
message.reply_to_message_idrequired
string (uuid) | nullThe message this one replies to, when the customer quoted one
message.created_atrequired
string (date-time)ISO 8601 timestamp
contactrequired
object
contact.idrequired
string (uuid)UUID
contact.namerequired
string | nullDisplay name
contact.phonerequired
string | nullE.164 phone number, when the channel has one
conversationrequired
object
conversation.idrequired
string (uuid)UUID
conversation.contact_idrequired
string (uuid)UUID
conversation.contactrequired
objectThe person on the other side
conversation.contact.idrequired
string (uuid)UUID
conversation.contact.namerequired
string | nullDisplay name
conversation.contact.phonerequired
string | nullE.164 phone number, when the channel has one
conversation.channelrequired
string (enum)The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp
conversation.connection_idrequired
string (uuid) | nullFor personal WhatsApp: the linked number this thread is on
conversation.statusrequired
string (enum)Where the thread sits in the inboxopenpendingclosed
conversation.assigned_torequired
string (uuid) | nullMember user id, or null when unassigned
conversation.ai_ownershiprequired
string (enum) | nullWho is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman
conversation.needs_attentionrequired
booleanThe assistant handed this thread to a human and nobody has picked it up
conversation.handoff_reasonrequired
string | nullWhy the thread was handed to a human, when it was
conversation.last_message_textrequired
string | nullPreview of the most recent message
conversation.last_message_atrequired
string (date-time) | null
conversation.unread_countrequired
integerInbound messages not yet marked read
conversation.created_atrequired
string (date-time)ISO 8601 timestamp
conversation.updated_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "message": {
    "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
    "conversation_id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
    "direction": "inbound",
    "sender_type": "customer",
    "type": "text",
    "text": "Is the clinic open on Saturday?",
    "media_url": null,
    "template_name": null,
    "status": "delivered",
    "channel_message_id": "wamid.HBgLOTcxNTAxMjM0NTY3FQIAEhgUM0E5QjZDRkQ0RkQ5QjM3RTQ3NkQA",
    "reply_to_message_id": null,
    "created_at": "2026-09-02T09:58:00.000Z"
  },
  "contact": {
    "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "name": "Ayesha Khan",
    "phone": "+971501234567"
  },
  "conversation": {
    "id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "contact": {
      "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
      "name": "Ayesha Khan",
      "phone": "+971501234567"
    },
    "channel": "whatsapp",
    "connection_id": null,
    "status": "open",
    "assigned_to": null,
    "ai_ownership": null,
    "needs_attention": false,
    "handoff_reason": null,
    "last_message_text": "Is the clinic open on Saturday?",
    "last_message_at": "2026-09-02T09:58:00.000Z",
    "unread_count": 1,
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T09:58:00.000Z"
  }
}

message.sent

A message was sent to a customer by a member, the assistant or the API. Internal AI-activity notes are never sent.

NameTypeDescription
messagerequired
object
message.idrequired
string (uuid)UUID
message.conversation_idrequired
string (uuid)UUID
message.directionrequired
string (enum)inbound = from the customerinboundoutbound
message.sender_typerequired
string (enum)bot = the assistant or an API key; agent = a member in the inboxcustomeragentbot
message.typerequired
string (enum)Content typetextimagedocumentaudiovideolocationtemplateinteractive
message.textrequired
string | nullBody text, or the caption of a media message
message.media_urlrequired
string | nullURL of the attached image, video, document or audio
message.template_namerequired
string | nullThe template this message was sent from, for template sends
message.statusrequired
string (enum)Delivery status as reported by the channelsendingsentdeliveredreadfailed
message.channel_message_idrequired
string | nullThe channel’s own id (a WhatsApp wamid, a Messenger mid)
message.reply_to_message_idrequired
string (uuid) | nullThe message this one replies to, when the customer quoted one
message.created_atrequired
string (date-time)ISO 8601 timestamp
contactrequired
object
contact.idrequired
string (uuid)UUID
contact.namerequired
string | nullDisplay name
contact.phonerequired
string | nullE.164 phone number, when the channel has one
conversationrequired
object
conversation.idrequired
string (uuid)UUID
conversation.contact_idrequired
string (uuid)UUID
conversation.contactrequired
objectThe person on the other side
conversation.contact.idrequired
string (uuid)UUID
conversation.contact.namerequired
string | nullDisplay name
conversation.contact.phonerequired
string | nullE.164 phone number, when the channel has one
conversation.channelrequired
string (enum)The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp
conversation.connection_idrequired
string (uuid) | nullFor personal WhatsApp: the linked number this thread is on
conversation.statusrequired
string (enum)Where the thread sits in the inboxopenpendingclosed
conversation.assigned_torequired
string (uuid) | nullMember user id, or null when unassigned
conversation.ai_ownershiprequired
string (enum) | nullWho is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman
conversation.needs_attentionrequired
booleanThe assistant handed this thread to a human and nobody has picked it up
conversation.handoff_reasonrequired
string | nullWhy the thread was handed to a human, when it was
conversation.last_message_textrequired
string | nullPreview of the most recent message
conversation.last_message_atrequired
string (date-time) | null
conversation.unread_countrequired
integerInbound messages not yet marked read
conversation.created_atrequired
string (date-time)ISO 8601 timestamp
conversation.updated_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "message": {
    "id": "8b9c0d1e-2f3a-4b4c-8d5e-6f7a8b9c0d1e",
    "conversation_id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
    "direction": "outbound",
    "sender_type": "agent",
    "type": "text",
    "text": "Yes — 9am to 2pm on Saturdays.",
    "media_url": null,
    "template_name": null,
    "status": "sent",
    "channel_message_id": "wamid.HBgLOTcxNTAxMjM0NTY3FQIAEhgUM0E5QjZDRkQ0RkQ5QjM3RTQ3NkQA",
    "reply_to_message_id": null,
    "created_at": "2026-09-02T09:58:00.000Z"
  },
  "contact": {
    "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "name": "Ayesha Khan",
    "phone": "+971501234567"
  },
  "conversation": {
    "id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "contact": {
      "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
      "name": "Ayesha Khan",
      "phone": "+971501234567"
    },
    "channel": "whatsapp",
    "connection_id": null,
    "status": "open",
    "assigned_to": null,
    "ai_ownership": null,
    "needs_attention": false,
    "handoff_reason": null,
    "last_message_text": "Is the clinic open on Saturday?",
    "last_message_at": "2026-09-02T09:58:00.000Z",
    "unread_count": 1,
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T09:58:00.000Z"
  }
}

message.status_updated

The channel reported a delivery status change for an outbound message (sent → delivered → read, or failed). Queued and delivered by the next drain pass.

NameTypeDescription
message_idrequired
string (uuid)UUID
channel_message_idrequired
string | nullThe channel’s own id (a WhatsApp wamid)
conversation_idrequired
string (uuid)UUID
contact_idrequired
string (uuid)UUID
channelrequired
string (enum)The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp
statusrequired
string (enum)Delivery status as reported by the channelsendingsentdeliveredreadfailed
data
{
  "message_id": "8b9c0d1e-2f3a-4b4c-8d5e-6f7a8b9c0d1e",
  "channel_message_id": "wamid.HBgLOTcxNTAxMjM0NTY3FQIAERgSNkY5QjZDRkQ0RkQ5QjM3RTQ3AA==",
  "conversation_id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
  "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
  "channel": "whatsapp",
  "status": "read"
}

conversation.created

A new thread was opened with a contact on a channel.

NameTypeDescription
conversationrequired
object
conversation.idrequired
string (uuid)UUID
conversation.contact_idrequired
string (uuid)UUID
conversation.contactrequired
objectThe person on the other side
conversation.contact.idrequired
string (uuid)UUID
conversation.contact.namerequired
string | nullDisplay name
conversation.contact.phonerequired
string | nullE.164 phone number, when the channel has one
conversation.channelrequired
string (enum)The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp
conversation.connection_idrequired
string (uuid) | nullFor personal WhatsApp: the linked number this thread is on
conversation.statusrequired
string (enum)Where the thread sits in the inboxopenpendingclosed
conversation.assigned_torequired
string (uuid) | nullMember user id, or null when unassigned
conversation.ai_ownershiprequired
string (enum) | nullWho is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman
conversation.needs_attentionrequired
booleanThe assistant handed this thread to a human and nobody has picked it up
conversation.handoff_reasonrequired
string | nullWhy the thread was handed to a human, when it was
conversation.last_message_textrequired
string | nullPreview of the most recent message
conversation.last_message_atrequired
string (date-time) | null
conversation.unread_countrequired
integerInbound messages not yet marked read
conversation.created_atrequired
string (date-time)ISO 8601 timestamp
conversation.updated_atrequired
string (date-time)ISO 8601 timestamp
contactrequired
object
contact.idrequired
string (uuid)UUID
contact.namerequired
string | nullDisplay name
contact.phonerequired
string | nullE.164 phone number, when the channel has one
data
{
  "conversation": {
    "id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "contact": {
      "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
      "name": "Ayesha Khan",
      "phone": "+971501234567"
    },
    "channel": "whatsapp",
    "connection_id": null,
    "status": "open",
    "assigned_to": null,
    "ai_ownership": null,
    "needs_attention": false,
    "handoff_reason": null,
    "last_message_text": "Is the clinic open on Saturday?",
    "last_message_at": "2026-09-02T09:58:00.000Z",
    "unread_count": 1,
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T09:58:00.000Z"
  },
  "contact": {
    "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "name": "Ayesha Khan",
    "phone": "+971501234567"
  }
}

conversation.status_changed

The thread moved between open, pending and closed.

NameTypeDescription
conversationrequired
object
conversation.idrequired
string (uuid)UUID
conversation.contact_idrequired
string (uuid)UUID
conversation.contactrequired
objectThe person on the other side
conversation.contact.idrequired
string (uuid)UUID
conversation.contact.namerequired
string | nullDisplay name
conversation.contact.phonerequired
string | nullE.164 phone number, when the channel has one
conversation.channelrequired
string (enum)The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp
conversation.connection_idrequired
string (uuid) | nullFor personal WhatsApp: the linked number this thread is on
conversation.statusrequired
string (enum)Where the thread sits in the inboxopenpendingclosed
conversation.assigned_torequired
string (uuid) | nullMember user id, or null when unassigned
conversation.ai_ownershiprequired
string (enum) | nullWho is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman
conversation.needs_attentionrequired
booleanThe assistant handed this thread to a human and nobody has picked it up
conversation.handoff_reasonrequired
string | nullWhy the thread was handed to a human, when it was
conversation.last_message_textrequired
string | nullPreview of the most recent message
conversation.last_message_atrequired
string (date-time) | null
conversation.unread_countrequired
integerInbound messages not yet marked read
conversation.created_atrequired
string (date-time)ISO 8601 timestamp
conversation.updated_atrequired
string (date-time)ISO 8601 timestamp
previous_statusrequired
string (enum)Where the thread sits in the inboxopenpendingclosed
statusrequired
string (enum)Where the thread sits in the inboxopenpendingclosed
data
{
  "conversation": {
    "id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "contact": {
      "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
      "name": "Ayesha Khan",
      "phone": "+971501234567"
    },
    "channel": "whatsapp",
    "connection_id": null,
    "status": "closed",
    "assigned_to": null,
    "ai_ownership": null,
    "needs_attention": false,
    "handoff_reason": null,
    "last_message_text": "Is the clinic open on Saturday?",
    "last_message_at": "2026-09-02T09:58:00.000Z",
    "unread_count": 1,
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T09:58:00.000Z"
  },
  "previous_status": "open",
  "status": "closed"
}

conversation.assigned

The thread was assigned to a member, reassigned, or unassigned (`assignee_user_id: null`).

NameTypeDescription
conversationrequired
object
conversation.idrequired
string (uuid)UUID
conversation.contact_idrequired
string (uuid)UUID
conversation.contactrequired
objectThe person on the other side
conversation.contact.idrequired
string (uuid)UUID
conversation.contact.namerequired
string | nullDisplay name
conversation.contact.phonerequired
string | nullE.164 phone number, when the channel has one
conversation.channelrequired
string (enum)The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp
conversation.connection_idrequired
string (uuid) | nullFor personal WhatsApp: the linked number this thread is on
conversation.statusrequired
string (enum)Where the thread sits in the inboxopenpendingclosed
conversation.assigned_torequired
string (uuid) | nullMember user id, or null when unassigned
conversation.ai_ownershiprequired
string (enum) | nullWho is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman
conversation.needs_attentionrequired
booleanThe assistant handed this thread to a human and nobody has picked it up
conversation.handoff_reasonrequired
string | nullWhy the thread was handed to a human, when it was
conversation.last_message_textrequired
string | nullPreview of the most recent message
conversation.last_message_atrequired
string (date-time) | null
conversation.unread_countrequired
integerInbound messages not yet marked read
conversation.created_atrequired
string (date-time)ISO 8601 timestamp
conversation.updated_atrequired
string (date-time)ISO 8601 timestamp
previous_assignee_user_idrequired
string (uuid) | null
assignee_user_idrequired
string (uuid) | nullnull = unassigned
data
{
  "conversation": {
    "id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "contact": {
      "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
      "name": "Ayesha Khan",
      "phone": "+971501234567"
    },
    "channel": "whatsapp",
    "connection_id": null,
    "status": "open",
    "assigned_to": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f",
    "ai_ownership": null,
    "needs_attention": false,
    "handoff_reason": null,
    "last_message_text": "Is the clinic open on Saturday?",
    "last_message_at": "2026-09-02T09:58:00.000Z",
    "unread_count": 1,
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T09:58:00.000Z"
  },
  "previous_assignee_user_id": null,
  "assignee_user_id": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f"
}

conversation.handoff

The assistant (or a member) flagged the thread as needing a human. `source` says who; `reason` is what they recorded.

NameTypeDescription
conversationrequired
object
conversation.idrequired
string (uuid)UUID
conversation.contact_idrequired
string (uuid)UUID
conversation.contactrequired
objectThe person on the other side
conversation.contact.idrequired
string (uuid)UUID
conversation.contact.namerequired
string | nullDisplay name
conversation.contact.phonerequired
string | nullE.164 phone number, when the channel has one
conversation.channelrequired
string (enum)The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp
conversation.connection_idrequired
string (uuid) | nullFor personal WhatsApp: the linked number this thread is on
conversation.statusrequired
string (enum)Where the thread sits in the inboxopenpendingclosed
conversation.assigned_torequired
string (uuid) | nullMember user id, or null when unassigned
conversation.ai_ownershiprequired
string (enum) | nullWho is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman
conversation.needs_attentionrequired
booleanThe assistant handed this thread to a human and nobody has picked it up
conversation.handoff_reasonrequired
string | nullWhy the thread was handed to a human, when it was
conversation.last_message_textrequired
string | nullPreview of the most recent message
conversation.last_message_atrequired
string (date-time) | null
conversation.unread_countrequired
integerInbound messages not yet marked read
conversation.created_atrequired
string (date-time)ISO 8601 timestamp
conversation.updated_atrequired
string (date-time)ISO 8601 timestamp
reasonrequired
string | null
sourcerequired
string (enum) | nullassistantmember
data
{
  "conversation": {
    "id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "contact": {
      "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
      "name": "Ayesha Khan",
      "phone": "+971501234567"
    },
    "channel": "whatsapp",
    "connection_id": null,
    "status": "open",
    "assigned_to": null,
    "ai_ownership": "human",
    "needs_attention": true,
    "handoff_reason": "Customer asked for a refund",
    "last_message_text": "Is the clinic open on Saturday?",
    "last_message_at": "2026-09-02T09:58:00.000Z",
    "unread_count": 1,
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T09:58:00.000Z"
  },
  "reason": "Customer asked for a refund",
  "source": "assistant"
}

contact.created

A contact was created — from an inbound message, the dashboard, a CSV import or the API.

NameTypeDescription
contactrequired
object
contact.idrequired
string (uuid)UUID
contact.namerequired
string | nullDisplay name
contact.phonerequired
string | nullE.164 phone number, or null for a contact reached only on Messenger/Instagram/web chat
contact.emailrequired
string | nullEmail address, when one is known
contact.companyrequired
string | nullCompany or organisation the contact belongs to
contact.avatar_urlrequired
string | nullProfile picture URL, when a channel supplied one
contact.tagsrequired
object[]Tags applied to this contact
contact.tags[].idrequired
string (uuid)UUID
contact.tags[].namerequired
stringTag name
contact.tags[].colorrequired
stringHex colour chosen in the dashboard
contact.custom_fieldsrequired
object[]Custom-field values set on this contact
contact.custom_fields[].field_idrequired
string (uuid)The custom field definition
contact.custom_fields[].namerequired
stringField name as shown in the dashboard
contact.custom_fields[].valuerequired
string | nullThe value for this contact
contact.created_atrequired
string (date-time)ISO 8601 timestamp
contact.updated_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "contact": {
    "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "name": "Ayesha Khan",
    "phone": "+971501234567",
    "email": "ayesha@example.com",
    "company": "Khan Trading LLC",
    "avatar_url": null,
    "tags": [
      {
        "id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
        "name": "VIP",
        "color": "#10b981"
      }
    ],
    "custom_fields": [
      {
        "field_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
        "name": "Source",
        "value": "Instagram ad"
      }
    ],
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T10:00:00.000Z"
  }
}

contact.updated

Name, phone, email, company or avatar changed. Edits in the same minute are collapsed into one event; tag changes have their own events.

NameTypeDescription
contactrequired
object
contact.idrequired
string (uuid)UUID
contact.namerequired
string | nullDisplay name
contact.phonerequired
string | nullE.164 phone number, or null for a contact reached only on Messenger/Instagram/web chat
contact.emailrequired
string | nullEmail address, when one is known
contact.companyrequired
string | nullCompany or organisation the contact belongs to
contact.avatar_urlrequired
string | nullProfile picture URL, when a channel supplied one
contact.tagsrequired
object[]Tags applied to this contact
contact.tags[].idrequired
string (uuid)UUID
contact.tags[].namerequired
stringTag name
contact.tags[].colorrequired
stringHex colour chosen in the dashboard
contact.custom_fieldsrequired
object[]Custom-field values set on this contact
contact.custom_fields[].field_idrequired
string (uuid)The custom field definition
contact.custom_fields[].namerequired
stringField name as shown in the dashboard
contact.custom_fields[].valuerequired
string | nullThe value for this contact
contact.created_atrequired
string (date-time)ISO 8601 timestamp
contact.updated_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "contact": {
    "id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "name": "Ayesha Khan",
    "phone": "+971501234567",
    "email": "ayesha@example.com",
    "company": "Khan Trading LLC",
    "avatar_url": null,
    "tags": [
      {
        "id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
        "name": "VIP",
        "color": "#10b981"
      }
    ],
    "custom_fields": [
      {
        "field_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
        "name": "Source",
        "value": "Instagram ad"
      }
    ],
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T10:00:00.000Z"
  }
}

contact.tag_added

A tag was applied to a contact.

NameTypeDescription
contact_idrequired
string (uuid)UUID
tagrequired
object
tag.idrequired
string (uuid)UUID
tag.namerequired
string
data
{
  "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
  "tag": {
    "id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
    "name": "VIP"
  }
}

contact.tag_removed

A tag was removed from a contact.

NameTypeDescription
contact_idrequired
string (uuid)UUID
tagrequired
object
tag.idrequired
string (uuid)UUID
tag.namerequired
string
data
{
  "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
  "tag": {
    "id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
    "name": "VIP"
  }
}

deal.created

A deal was added to a pipeline.

NameTypeDescription
dealrequired
object
deal.idrequired
string (uuid)UUID
deal.titlerequired
stringWhat the deal is for
deal.valuerequired
numberDeal value as a number (the API coerces the decimal column)
deal.currencyrequired
string | nullISO 4217 code
deal.statusrequired
string (enum)Outcome; a deal is open until it is closedopenwonlost
deal.pipeline_idrequired
string (uuid)UUID
deal.stage_idrequired
string (uuid)UUID
deal.stagerequired
object | nullThe stage the deal currently sits in
deal.stage.idrequired
string (uuid)UUID
deal.stage.namerequired
stringStage name
deal.contact_idrequired
string (uuid) | null
deal.conversation_idrequired
string (uuid) | null
deal.assigned_torequired
string (uuid) | nullMember user id
deal.notesrequired
string | nullFree-text notes on the deal
deal.expected_close_daterequired
string | nullYYYY-MM-DD
deal.created_atrequired
string (date-time)ISO 8601 timestamp
deal.updated_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "deal": {
    "id": "e5f6a7b8-c9d0-4e1f-8a3b-4c5d6e7f8a9b",
    "title": "Khan Trading — annual package",
    "value": 12000,
    "currency": "AED",
    "status": "open",
    "pipeline_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "stage_id": "c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f",
    "stage": {
      "id": "c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f",
      "name": "Qualified"
    },
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "conversation_id": null,
    "assigned_to": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f",
    "notes": null,
    "expected_close_date": "2026-10-15",
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T10:00:00.000Z"
  }
}

deal.stage_changed

The deal moved between stages. Every move is reported, including moving back to a stage it has already been in.

NameTypeDescription
dealrequired
object
deal.idrequired
string (uuid)UUID
deal.titlerequired
stringWhat the deal is for
deal.valuerequired
numberDeal value as a number (the API coerces the decimal column)
deal.currencyrequired
string | nullISO 4217 code
deal.statusrequired
string (enum)Outcome; a deal is open until it is closedopenwonlost
deal.pipeline_idrequired
string (uuid)UUID
deal.stage_idrequired
string (uuid)UUID
deal.stagerequired
object | nullThe stage the deal currently sits in
deal.stage.idrequired
string (uuid)UUID
deal.stage.namerequired
stringStage name
deal.contact_idrequired
string (uuid) | null
deal.conversation_idrequired
string (uuid) | null
deal.assigned_torequired
string (uuid) | nullMember user id
deal.notesrequired
string | nullFree-text notes on the deal
deal.expected_close_daterequired
string | nullYYYY-MM-DD
deal.created_atrequired
string (date-time)ISO 8601 timestamp
deal.updated_atrequired
string (date-time)ISO 8601 timestamp
previous_stagerequired
object | null
previous_stage.idrequired
string (uuid)UUID
previous_stage.namerequired
string
stagerequired
object
stage.idrequired
string (uuid)UUID
stage.namerequired
string
data
{
  "deal": {
    "id": "e5f6a7b8-c9d0-4e1f-8a3b-4c5d6e7f8a9b",
    "title": "Khan Trading — annual package",
    "value": 12000,
    "currency": "AED",
    "status": "open",
    "pipeline_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "stage_id": "d4e5f6a7-b8c9-4d0e-9f2a-3b4c5d6e7f8a",
    "stage": {
      "id": "d4e5f6a7-b8c9-4d0e-9f2a-3b4c5d6e7f8a",
      "name": "Proposal"
    },
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "conversation_id": null,
    "assigned_to": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f",
    "notes": null,
    "expected_close_date": "2026-10-15",
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T10:00:00.000Z"
  },
  "previous_stage": {
    "id": "c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f",
    "name": "Qualified"
  },
  "stage": {
    "id": "d4e5f6a7-b8c9-4d0e-9f2a-3b4c5d6e7f8a",
    "name": "Proposal"
  }
}

deal.won

The deal was closed as won.

NameTypeDescription
dealrequired
object
deal.idrequired
string (uuid)UUID
deal.titlerequired
stringWhat the deal is for
deal.valuerequired
numberDeal value as a number (the API coerces the decimal column)
deal.currencyrequired
string | nullISO 4217 code
deal.statusrequired
string (enum)Outcome; a deal is open until it is closedopenwonlost
deal.pipeline_idrequired
string (uuid)UUID
deal.stage_idrequired
string (uuid)UUID
deal.stagerequired
object | nullThe stage the deal currently sits in
deal.stage.idrequired
string (uuid)UUID
deal.stage.namerequired
stringStage name
deal.contact_idrequired
string (uuid) | null
deal.conversation_idrequired
string (uuid) | null
deal.assigned_torequired
string (uuid) | nullMember user id
deal.notesrequired
string | nullFree-text notes on the deal
deal.expected_close_daterequired
string | nullYYYY-MM-DD
deal.created_atrequired
string (date-time)ISO 8601 timestamp
deal.updated_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "deal": {
    "id": "e5f6a7b8-c9d0-4e1f-8a3b-4c5d6e7f8a9b",
    "title": "Khan Trading — annual package",
    "value": 12000,
    "currency": "AED",
    "status": "won",
    "pipeline_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "stage_id": "c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f",
    "stage": {
      "id": "c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f",
      "name": "Qualified"
    },
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "conversation_id": null,
    "assigned_to": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f",
    "notes": null,
    "expected_close_date": "2026-10-15",
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T10:00:00.000Z"
  }
}

deal.lost

The deal was closed as lost.

NameTypeDescription
dealrequired
object
deal.idrequired
string (uuid)UUID
deal.titlerequired
stringWhat the deal is for
deal.valuerequired
numberDeal value as a number (the API coerces the decimal column)
deal.currencyrequired
string | nullISO 4217 code
deal.statusrequired
string (enum)Outcome; a deal is open until it is closedopenwonlost
deal.pipeline_idrequired
string (uuid)UUID
deal.stage_idrequired
string (uuid)UUID
deal.stagerequired
object | nullThe stage the deal currently sits in
deal.stage.idrequired
string (uuid)UUID
deal.stage.namerequired
stringStage name
deal.contact_idrequired
string (uuid) | null
deal.conversation_idrequired
string (uuid) | null
deal.assigned_torequired
string (uuid) | nullMember user id
deal.notesrequired
string | nullFree-text notes on the deal
deal.expected_close_daterequired
string | nullYYYY-MM-DD
deal.created_atrequired
string (date-time)ISO 8601 timestamp
deal.updated_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "deal": {
    "id": "e5f6a7b8-c9d0-4e1f-8a3b-4c5d6e7f8a9b",
    "title": "Khan Trading — annual package",
    "value": 12000,
    "currency": "AED",
    "status": "lost",
    "pipeline_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "stage_id": "c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f",
    "stage": {
      "id": "c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f",
      "name": "Qualified"
    },
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "conversation_id": null,
    "assigned_to": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f",
    "notes": null,
    "expected_close_date": "2026-10-15",
    "created_at": "2026-09-01T08:15:00.000Z",
    "updated_at": "2026-09-02T10:00:00.000Z"
  }
}

broadcast.completed

A broadcast finished sending (`status: sent`) or stopped (`status: failed`). The counts are final at this point.

NameTypeDescription
broadcastrequired
object
broadcast.idrequired
string (uuid)UUID
broadcast.namerequired
stringCampaign name, as shown in the dashboard
broadcast.kindrequired
string (enum)template = an approved WhatsApp template; free_text = a personal-WhatsApp messagetemplatefree_text
broadcast.channelrequired
stringwhatsapp | personal_whatsapp
broadcast.templaterequired
object | nullThe template a template broadcast sends
broadcast.template.namerequired
string
broadcast.template.languagerequired
string
broadcast.statusrequired
string (enum)draft → scheduled → sending → sent | faileddraftscheduledsendingsentfailed
broadcast.scheduled_atrequired
string (date-time) | nullWhen it was (or will be) started
broadcast.countsrequired
objectLive delivery counters — poll this endpoint for progress
broadcast.counts.totalrequired
integerRecipients materialised
broadcast.counts.sentrequired
integerHanded to the channel
broadcast.counts.deliveredrequired
integerConfirmed delivered to the handset
broadcast.counts.readrequired
integerConfirmed read
broadcast.counts.repliedrequired
integerRecipients who replied
broadcast.counts.failedrequired
integerRejected by the channel
broadcast.created_atrequired
string (date-time)ISO 8601 timestamp
broadcast.updated_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "broadcast": {
    "id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
    "name": "September promo",
    "kind": "template",
    "channel": "whatsapp",
    "template": {
      "name": "sept_promo",
      "language": "en_US"
    },
    "status": "sent",
    "scheduled_at": "2026-09-02T09:00:00.000Z",
    "counts": {
      "total": 250,
      "sent": 248,
      "delivered": 240,
      "read": 180,
      "replied": 12,
      "failed": 2
    },
    "created_at": "2026-09-01T16:00:00.000Z",
    "updated_at": "2026-09-02T09:20:00.000Z"
  }
}

booking.created

A booking was made — by staff, by the assistant, or from the public booking page.

NameTypeDescription
bookingrequired
object
booking.idrequired
string (uuid)UUID
booking.contact_idrequired
string (uuid) | nullThe contact this booking is for
booking.assignee_user_idrequired
string (uuid) | nullThe member who owns the slot
booking.typerequired
object | nullThe booking type, when one is set
booking.type.idrequired
string (uuid)UUID
booking.type.namerequired
string
booking.titlerequired
string | null
booking.locationrequired
string | nullSnapshotted from the type at booking time
booking.starts_atrequired
string (date-time)ISO 8601 timestamp
booking.ends_atrequired
string (date-time)ISO 8601 timestamp
booking.statusrequired
string (enum)scheduledconfirmedcompletedno_showcancelled
booking.sourcerequired
string (enum)Who booked itstaffaipublic
booking.cancel_reasonrequired
string | null
booking.completed_atrequired
string (date-time) | null
booking.created_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "booking": {
    "id": "c0ffee00-1234-4abc-9def-000000000001",
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "assignee_user_id": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f",
    "type": {
      "id": "b00c1e00-1234-4abc-9def-000000000002",
      "name": "Consultation"
    },
    "title": "Consultation — Ayesha Khan",
    "location": "Clinic, Room 2",
    "starts_at": "2026-09-05T10:00:00.000Z",
    "ends_at": "2026-09-05T10:30:00.000Z",
    "status": "scheduled",
    "source": "public",
    "cancel_reason": null,
    "completed_at": null,
    "created_at": "2026-09-02T10:05:00.000Z"
  }
}

booking.cancelled

A booking was cancelled. `cancel_reason` carries what was recorded.

NameTypeDescription
bookingrequired
object
booking.idrequired
string (uuid)UUID
booking.contact_idrequired
string (uuid) | nullThe contact this booking is for
booking.assignee_user_idrequired
string (uuid) | nullThe member who owns the slot
booking.typerequired
object | nullThe booking type, when one is set
booking.type.idrequired
string (uuid)UUID
booking.type.namerequired
string
booking.titlerequired
string | null
booking.locationrequired
string | nullSnapshotted from the type at booking time
booking.starts_atrequired
string (date-time)ISO 8601 timestamp
booking.ends_atrequired
string (date-time)ISO 8601 timestamp
booking.statusrequired
string (enum)scheduledconfirmedcompletedno_showcancelled
booking.sourcerequired
string (enum)Who booked itstaffaipublic
booking.cancel_reasonrequired
string | null
booking.completed_atrequired
string (date-time) | null
booking.created_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "booking": {
    "id": "c0ffee00-1234-4abc-9def-000000000001",
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "assignee_user_id": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f",
    "type": {
      "id": "b00c1e00-1234-4abc-9def-000000000002",
      "name": "Consultation"
    },
    "title": "Consultation — Ayesha Khan",
    "location": "Clinic, Room 2",
    "starts_at": "2026-09-05T10:00:00.000Z",
    "ends_at": "2026-09-05T10:30:00.000Z",
    "status": "cancelled",
    "source": "public",
    "cancel_reason": "Customer rescheduled by phone",
    "completed_at": null,
    "created_at": "2026-09-02T10:05:00.000Z"
  }
}

booking.completed

A booking was marked completed.

NameTypeDescription
bookingrequired
object
booking.idrequired
string (uuid)UUID
booking.contact_idrequired
string (uuid) | nullThe contact this booking is for
booking.assignee_user_idrequired
string (uuid) | nullThe member who owns the slot
booking.typerequired
object | nullThe booking type, when one is set
booking.type.idrequired
string (uuid)UUID
booking.type.namerequired
string
booking.titlerequired
string | null
booking.locationrequired
string | nullSnapshotted from the type at booking time
booking.starts_atrequired
string (date-time)ISO 8601 timestamp
booking.ends_atrequired
string (date-time)ISO 8601 timestamp
booking.statusrequired
string (enum)scheduledconfirmedcompletedno_showcancelled
booking.sourcerequired
string (enum)Who booked itstaffaipublic
booking.cancel_reasonrequired
string | null
booking.completed_atrequired
string (date-time) | null
booking.created_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "booking": {
    "id": "c0ffee00-1234-4abc-9def-000000000001",
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "assignee_user_id": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f",
    "type": {
      "id": "b00c1e00-1234-4abc-9def-000000000002",
      "name": "Consultation"
    },
    "title": "Consultation — Ayesha Khan",
    "location": "Clinic, Room 2",
    "starts_at": "2026-09-05T10:00:00.000Z",
    "ends_at": "2026-09-05T10:30:00.000Z",
    "status": "completed",
    "source": "public",
    "cancel_reason": null,
    "completed_at": "2026-09-05T10:35:00.000Z",
    "created_at": "2026-09-02T10:05:00.000Z"
  }
}

inquiry.created

Someone wrote in through a website contact form or the API. Delivered immediately from the intake path, not on the next sweep, because a lead that arrives ten minutes late has usually gone to whoever answered first. A submission from a blocked sender is never recorded and never delivered.

NameTypeDescription
inquiryrequired
object
inquiry.idrequired
string (uuid)UUID
inquiry.statusrequired
string (enum)Where the inquiry sits in triagenewin_progressclosedspam
inquiry.topicrequired
stringYour own category for the inquiry — free text, e.g. "Billing"
inquiry.namerequired
stringWho sent it
inquiry.emailrequired
string | nullSender email, lowercased; null if the form collected only a phone
inquiry.phonerequired
string | nullSender phone; null if the form collected only an email
inquiry.companyrequired
string | nullSender company, when the form asks
inquiry.messagerequired
stringWhat they wrote
inquiry.sourcerequired
string | nullWhich form this came from, e.g. "Pricing page"
inquiry.source_pathrequired
string | nullThe page path the form was on
inquiry.metadatarequired
map<string, string | number | boolean | null>Any additional form fields, verbatim — flat key/value pairs. Shown in the inbox and included in the CSV export.
inquiry.first_touched_atrequired
string (date-time) | nullWhen someone first acted on it; null while untouched
inquiry.created_atrequired
string (date-time)ISO 8601 timestamp
inquiry.updated_atrequired
string (date-time)ISO 8601 timestamp
data
{
  "inquiry": {
    "id": "a3f1c2d4-5e6b-4f78-9a0b-1c2d3e4f5a6b",
    "status": "new",
    "topic": "Quote request",
    "name": "Ayesha Khan",
    "email": "ayesha@example.com",
    "phone": "+971501234567",
    "company": "Khan Trading LLC",
    "message": "Do you cover Jumeirah? Looking for a quote for two units.",
    "source": "Pricing page",
    "source_path": "/pricing",
    "metadata": {
      "Budget": "AED 10,000–25,000",
      "Preferred contact": "WhatsApp",
      "Units": 2
    },
    "first_touched_at": null,
    "created_at": "2026-09-02T10:05:00.000Z",
    "updated_at": "2026-09-02T10:05:00.000Z"
  }
}

inquiry.status_changed

The inquiry moved between new, in progress, closed and spam — from the dashboard, the API or the assistant. Every move is reported, including moving back to a state it has already been in. Queued and delivered by the next drain pass.

NameTypeDescription
inquiryrequired
object
inquiry.idrequired
string (uuid)UUID
inquiry.statusrequired
string (enum)Where the inquiry sits in triagenewin_progressclosedspam
inquiry.topicrequired
stringYour own category for the inquiry — free text, e.g. "Billing"
inquiry.namerequired
stringWho sent it
inquiry.emailrequired
string | nullSender email, lowercased; null if the form collected only a phone
inquiry.phonerequired
string | nullSender phone; null if the form collected only an email
inquiry.companyrequired
string | nullSender company, when the form asks
inquiry.messagerequired
stringWhat they wrote
inquiry.sourcerequired
string | nullWhich form this came from, e.g. "Pricing page"
inquiry.source_pathrequired
string | nullThe page path the form was on
inquiry.metadatarequired
map<string, string | number | boolean | null>Any additional form fields, verbatim — flat key/value pairs. Shown in the inbox and included in the CSV export.
inquiry.first_touched_atrequired
string (date-time) | nullWhen someone first acted on it; null while untouched
inquiry.created_atrequired
string (date-time)ISO 8601 timestamp
inquiry.updated_atrequired
string (date-time)ISO 8601 timestamp
previous_statusrequired
string (enum) | nullnewin_progressclosedspam
statusrequired
string (enum)Where the inquiry sits in triagenewin_progressclosedspam
data
{
  "inquiry": {
    "id": "a3f1c2d4-5e6b-4f78-9a0b-1c2d3e4f5a6b",
    "status": "in_progress",
    "topic": "Quote request",
    "name": "Ayesha Khan",
    "email": "ayesha@example.com",
    "phone": "+971501234567",
    "company": "Khan Trading LLC",
    "message": "Do you cover Jumeirah? Looking for a quote for two units.",
    "source": "Pricing page",
    "source_path": "/pricing",
    "metadata": {
      "Budget": "AED 10,000–25,000",
      "Preferred contact": "WhatsApp",
      "Units": 2
    },
    "first_touched_at": "2026-09-02T11:15:00.000Z",
    "created_at": "2026-09-02T10:05:00.000Z",
    "updated_at": "2026-09-02T10:05:00.000Z"
  },
  "previous_status": "new",
  "status": "in_progress"
}

assistant.run_completed

An assistant finished what it was asked to do. `result.outcome` says how it went and `result.summary` says what happened in a line. A sub-agent does not fire this — the parent run reports for the whole tree.

NameTypeDescription
runrequired
object
run.idrequired
string (uuid)Run id
run.assistant_idrequired
string (uuid)The assistant running
run.assistantrequired
object | nullThe assistant, or null if it has since been deleted
run.assistant.idrequired
string (uuid)Assistant id
run.assistant.namerequired
stringAssistant name
run.assistant.colorrequired
string | nullAccent colour
run.statusrequired
string (enum)Where the run is in its lifecyclequeuedrunningwaiting_replywaiting_approvalwaiting_childrencompletedfailedcancelled
run.goalrequired
stringWhat this run was asked to do
run.triggerrequired
objectWhere the run came from
run.trigger.typerequired
stringOne of manual, inbound_message, schedule, event, webhook or delegation
run.trigger.trigger_idrequired
string (uuid) | nullThe assistant trigger, when one fired it
run.trigger.trigger_namerequired
string | nullThat trigger name
run.trigger.event_keyrequired
string | nullThe event key, for an event trigger
run.contact_idrequired
string (uuid) | nullThe contact it concerns, if any
run.conversation_idrequired
string (uuid) | nullThe thread it may answer in, if any
run.parent_run_idrequired
string (uuid) | nullSet when this is a sub-agent of another run
run.root_run_idrequired
string (uuid) | nullThe top of the delegation tree
run.depthrequired
integer0 for a root run
run.resultrequired
object | nullPresent once the run has ended
run.result.outcomerequired
string | nullHow it ended, in one word
run.result.summaryrequired
string | nullOne line on what happened
run.tick_countrequired
integerModel turns this run has taken
run.cost_usdrequired
numberBilled AI spend attributed to this run
run.error_coderequired
string | nullMachine code when the run failed
run.error_messagerequired
string | nullOperator-facing failure text
run.started_atrequired
string (date-time)When the run was created
run.ended_atrequired
string (date-time) | nullWhen it reached a terminal status
run.created_atrequired
string (date-time)When the row was written
data
{
  "run": {
    "id": "3f9a1c2e-5b7d-4e8f-9a0b-1c2d3e4f5a6b",
    "assistant_id": "8f2c1d3e-4a5b-4c6d-9e0f-1a2b3c4d5e6f",
    "assistant": {
      "id": "8f2c1d3e-4a5b-4c6d-9e0f-1a2b3c4d5e6f",
      "name": "Front desk",
      "color": "#0ea5e9"
    },
    "status": "completed",
    "goal": "Follow up with Ayesha about her missed appointment and offer to rebook.",
    "trigger": {
      "type": "manual",
      "trigger_id": null,
      "trigger_name": null,
      "event_key": null
    },
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "conversation_id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
    "parent_run_id": null,
    "root_run_id": "3f9a1c2e-5b7d-4e8f-9a0b-1c2d3e4f5a6b",
    "depth": 0,
    "result": {
      "outcome": "success",
      "summary": "Rebooked for Saturday at 11:00."
    },
    "tick_count": 4,
    "cost_usd": 0.0182,
    "error_code": null,
    "error_message": null,
    "started_at": "2026-09-02T09:00:00.000Z",
    "ended_at": "2026-09-02T09:03:12.000Z",
    "created_at": "2026-09-02T09:00:00.000Z"
  }
}

assistant.run_failed

A run stopped without finishing — it errored, hit a guardrail, ran out of budget, or somebody cancelled it. `status` tells cancellation from failure, and `error_code` says why.

NameTypeDescription
runrequired
object
run.idrequired
string (uuid)Run id
run.assistant_idrequired
string (uuid)The assistant running
run.assistantrequired
object | nullThe assistant, or null if it has since been deleted
run.assistant.idrequired
string (uuid)Assistant id
run.assistant.namerequired
stringAssistant name
run.assistant.colorrequired
string | nullAccent colour
run.statusrequired
string (enum)Where the run is in its lifecyclequeuedrunningwaiting_replywaiting_approvalwaiting_childrencompletedfailedcancelled
run.goalrequired
stringWhat this run was asked to do
run.triggerrequired
objectWhere the run came from
run.trigger.typerequired
stringOne of manual, inbound_message, schedule, event, webhook or delegation
run.trigger.trigger_idrequired
string (uuid) | nullThe assistant trigger, when one fired it
run.trigger.trigger_namerequired
string | nullThat trigger name
run.trigger.event_keyrequired
string | nullThe event key, for an event trigger
run.contact_idrequired
string (uuid) | nullThe contact it concerns, if any
run.conversation_idrequired
string (uuid) | nullThe thread it may answer in, if any
run.parent_run_idrequired
string (uuid) | nullSet when this is a sub-agent of another run
run.root_run_idrequired
string (uuid) | nullThe top of the delegation tree
run.depthrequired
integer0 for a root run
run.resultrequired
object | nullPresent once the run has ended
run.result.outcomerequired
string | nullHow it ended, in one word
run.result.summaryrequired
string | nullOne line on what happened
run.tick_countrequired
integerModel turns this run has taken
run.cost_usdrequired
numberBilled AI spend attributed to this run
run.error_coderequired
string | nullMachine code when the run failed
run.error_messagerequired
string | nullOperator-facing failure text
run.started_atrequired
string (date-time)When the run was created
run.ended_atrequired
string (date-time) | nullWhen it reached a terminal status
run.created_atrequired
string (date-time)When the row was written
data
{
  "run": {
    "id": "3f9a1c2e-5b7d-4e8f-9a0b-1c2d3e4f5a6b",
    "assistant_id": "8f2c1d3e-4a5b-4c6d-9e0f-1a2b3c4d5e6f",
    "assistant": {
      "id": "8f2c1d3e-4a5b-4c6d-9e0f-1a2b3c4d5e6f",
      "name": "Front desk",
      "color": "#0ea5e9"
    },
    "status": "failed",
    "goal": "Follow up with Ayesha about her missed appointment and offer to rebook.",
    "trigger": {
      "type": "manual",
      "trigger_id": null,
      "trigger_name": null,
      "event_key": null
    },
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "conversation_id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
    "parent_run_id": null,
    "root_run_id": "3f9a1c2e-5b7d-4e8f-9a0b-1c2d3e4f5a6b",
    "depth": 0,
    "result": null,
    "tick_count": 4,
    "cost_usd": 0.0182,
    "error_code": "budget_blocked",
    "error_message": "The monthly AI budget is exhausted.",
    "started_at": "2026-09-02T09:00:00.000Z",
    "ended_at": "2026-09-02T09:02:00.000Z",
    "created_at": "2026-09-02T09:00:00.000Z"
  }
}

assistant.approval_requested

A run stopped to ask a person something, and is parked until it is answered or `expires_at` passes. It holds that conversation’s only run slot while it waits, so this is the event worth acting on quickly. Answer with POST /api/v1/asks/{id}/answer.

NameTypeDescription
askrequired
object
ask.idrequired
string (uuid)Ask id
ask.run_idrequired
string (uuid)The run parked on this question
ask.assistant_idrequired
string (uuid) | nullThe assistant that asked
ask.kindrequired
string (enum)What kind of answer the run needsapprovalchoiceinputtool_call
ask.statusrequired
string (enum)Only a pending ask can be answeredpendingansweredexpiredcancelled
ask.questionrequired
stringWhat the assistant is asking
ask.contextrequired
string | nullBackground it supplied with the question
ask.optionsrequired
object[]For a choice: the options offered. Empty for every other kind
ask.options[].idrequired
stringPass this back as option_id
ask.options[].labelrequired
stringWhat to show a human
ask.options[].descriptionrequired
string | nullExtra detail, if the assistant gave any
ask.tool_idrequired
string | nullFor a tool_call: the tool the model proposed and has NOT run
ask.tool_inputrequired
map<string, any> | nullThe arguments it proposed
ask.min_rolerequired
stringThe dashboard role floor for answering. NOT enforced for API keys, where the asks:answer scope is the gate
ask.expires_atrequired
string (date-time)After this the on_timeout rule applies automatically
ask.on_timeoutrequired
string (enum)What happens if nobody answers in timerejectapprovehandofffail
ask.decisionrequired
string | nullThe decision recorded, once answered
ask.answerrequired
map<string, any> | nullThe answer recorded
ask.answered_byrequired
string (uuid) | nullThe member who answered. Null when an API key, a timeout or a withdrawal settled it
ask.answered_atrequired
string (date-time) | nullWhen it was answered
ask.answer_sourcerequired
string (enum) | nullHow it settledhumantimeoutcancel
ask.created_atrequired
string (date-time)When the assistant asked
ask.updated_atrequired
string (date-time)When it last changed
data
{
  "ask": {
    "id": "6d7e8f9a-0b1c-4d2e-8f3a-4b5c6d7e8f9a",
    "run_id": "3f9a1c2e-5b7d-4e8f-9a0b-1c2d3e4f5a6b",
    "assistant_id": "8f2c1d3e-4a5b-4c6d-9e0f-1a2b3c4d5e6f",
    "kind": "approval",
    "status": "pending",
    "question": "Offer Ayesha a free reschedule outside the 24-hour window?",
    "context": "She cancelled 3 hours before her slot and has been a customer for two years.",
    "options": [],
    "tool_id": null,
    "tool_input": null,
    "min_role": "admin",
    "expires_at": "2026-09-03T09:00:00.000Z",
    "on_timeout": "reject",
    "decision": null,
    "answer": null,
    "answered_by": null,
    "answered_at": null,
    "answer_source": null,
    "created_at": "2026-09-02T09:01:40.000Z",
    "updated_at": "2026-09-02T09:01:40.000Z"
  }
}