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.
| Event | Resource | Fires when |
|---|---|---|
| message.received | message | A customer sent a message on any channel. Delivered immediately from the inbound path, alongside the assistant dispatch. |
| message.sent | message | A message was sent to a customer by a member, the assistant or the API. Internal AI-activity notes are never sent. |
| message.status_updated | message | The channel reported a delivery status change for an outbound message (sent → delivered → read, or failed). Queued and delivered by the next drain pass. |
| conversation.created | conversation | A new thread was opened with a contact on a channel. |
| conversation.status_changed | conversation | The thread moved between open, pending and closed. |
| conversation.assigned | conversation | The thread was assigned to a member, reassigned, or unassigned (`assignee_user_id: null`). |
| conversation.handoff | conversation | The assistant (or a member) flagged the thread as needing a human. `source` says who; `reason` is what they recorded. |
| contact.created | contact | A contact was created — from an inbound message, the dashboard, a CSV import or the API. |
| contact.updated | contact | Name, phone, email, company or avatar changed. Edits in the same minute are collapsed into one event; tag changes have their own events. |
| contact.tag_added | contact | A tag was applied to a contact. |
| contact.tag_removed | contact | A tag was removed from a contact. |
| deal.created | deal | A deal was added to a pipeline. |
| deal.stage_changed | deal | The deal moved between stages. Every move is reported, including moving back to a stage it has already been in. |
| deal.won | deal | The deal was closed as won. |
| deal.lost | deal | The deal was closed as lost. |
| broadcast.completed | broadcast | A broadcast finished sending (`status: sent`) or stopped (`status: failed`). The counts are final at this point. |
| booking.created | booking | A booking was made — by staff, by the assistant, or from the public booking page. |
| booking.cancelled | booking | A booking was cancelled. `cancel_reason` carries what was recorded. |
| booking.completed | booking | A booking was marked completed. |
| inquiry.created | inquiry | 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. |
| inquiry.status_changed | inquiry | 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. |
| assistant.run_completed | run | 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. |
| assistant.run_failed | run | 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. |
| assistant.approval_requested | run | 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. |
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.
{
"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"
}
}
}| Name | Type | Description |
|---|---|---|
idrequired | string | Unique event id — dedupe on this; delivery is at-least-once |
typerequired | string | The event type, e.g. contact.created |
api_versionrequired | string | The 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 | any | The event-specific payload |
Headers
| Header | Meaning |
|---|---|
| X-Raabta-Signature | t=<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-Timestamp | The same timestamp as t=, on its own, as Unix seconds. |
| X-Raabta-Event | The event type, so a receiver can route before parsing. |
| X-Raabta-Delivery | A unique id for this delivery. Retries of the same event carry the same envelope id but a new delivery id. |
| X-Raabta-Attempt | Which 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:
- Read the raw request bytes. Do not parse and re-serialise — any change to whitespace or key order changes the signature.
- Split
X-Raabta-Signatureon commas; taketand everyv1. - Reject if
|now − t|exceeds 300 seconds — that bounds how long a captured delivery can be replayed. - Compute the HMAC and compare it to each
v1with a constant-time comparison. Any match is valid.
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
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:
| Attempt | Delay after the previous attempt |
|---|---|
| 2 | 1 min |
| 3 | 5 min |
| 4 | 30 min |
| 5 | 2 h |
| 6 | 12 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.
| Name | Type | Description |
|---|---|---|
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 | null | Body text, or the caption of a media message |
message.media_urlrequired | string | null | URL of the attached image, video, document or audio |
message.template_namerequired | string | null | The 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 | null | The channel’s own id (a WhatsApp wamid, a Messenger mid) |
message.reply_to_message_idrequired | string (uuid) | null | The 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 | null | Display name |
contact.phonerequired | string | null | E.164 phone number, when the channel has one |
conversationrequired | object | |
conversation.idrequired | string (uuid) | UUID |
conversation.contact_idrequired | string (uuid) | UUID |
conversation.contactrequired | object | The person on the other side |
conversation.contact.idrequired | string (uuid) | UUID |
conversation.contact.namerequired | string | null | Display name |
conversation.contact.phonerequired | string | null | E.164 phone number, when the channel has one |
conversation.channelrequired | string (enum) | The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp |
conversation.connection_idrequired | string (uuid) | null | For 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) | null | Member user id, or null when unassigned |
conversation.ai_ownershiprequired | string (enum) | null | Who is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman |
conversation.needs_attentionrequired | boolean | The assistant handed this thread to a human and nobody has picked it up |
conversation.handoff_reasonrequired | string | null | Why the thread was handed to a human, when it was |
conversation.last_message_textrequired | string | null | Preview of the most recent message |
conversation.last_message_atrequired | string (date-time) | null | |
conversation.unread_countrequired | integer | Inbound messages not yet marked read |
conversation.created_atrequired | string (date-time) | ISO 8601 timestamp |
conversation.updated_atrequired | string (date-time) | ISO 8601 timestamp |
{
"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.
| Name | Type | Description |
|---|---|---|
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 | null | Body text, or the caption of a media message |
message.media_urlrequired | string | null | URL of the attached image, video, document or audio |
message.template_namerequired | string | null | The 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 | null | The channel’s own id (a WhatsApp wamid, a Messenger mid) |
message.reply_to_message_idrequired | string (uuid) | null | The 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 | null | Display name |
contact.phonerequired | string | null | E.164 phone number, when the channel has one |
conversationrequired | object | |
conversation.idrequired | string (uuid) | UUID |
conversation.contact_idrequired | string (uuid) | UUID |
conversation.contactrequired | object | The person on the other side |
conversation.contact.idrequired | string (uuid) | UUID |
conversation.contact.namerequired | string | null | Display name |
conversation.contact.phonerequired | string | null | E.164 phone number, when the channel has one |
conversation.channelrequired | string (enum) | The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp |
conversation.connection_idrequired | string (uuid) | null | For 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) | null | Member user id, or null when unassigned |
conversation.ai_ownershiprequired | string (enum) | null | Who is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman |
conversation.needs_attentionrequired | boolean | The assistant handed this thread to a human and nobody has picked it up |
conversation.handoff_reasonrequired | string | null | Why the thread was handed to a human, when it was |
conversation.last_message_textrequired | string | null | Preview of the most recent message |
conversation.last_message_atrequired | string (date-time) | null | |
conversation.unread_countrequired | integer | Inbound messages not yet marked read |
conversation.created_atrequired | string (date-time) | ISO 8601 timestamp |
conversation.updated_atrequired | string (date-time) | ISO 8601 timestamp |
{
"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.
| Name | Type | Description |
|---|---|---|
message_idrequired | string (uuid) | UUID |
channel_message_idrequired | string | null | The 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 |
{
"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.
| Name | Type | Description |
|---|---|---|
conversationrequired | object | |
conversation.idrequired | string (uuid) | UUID |
conversation.contact_idrequired | string (uuid) | UUID |
conversation.contactrequired | object | The person on the other side |
conversation.contact.idrequired | string (uuid) | UUID |
conversation.contact.namerequired | string | null | Display name |
conversation.contact.phonerequired | string | null | E.164 phone number, when the channel has one |
conversation.channelrequired | string (enum) | The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp |
conversation.connection_idrequired | string (uuid) | null | For 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) | null | Member user id, or null when unassigned |
conversation.ai_ownershiprequired | string (enum) | null | Who is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman |
conversation.needs_attentionrequired | boolean | The assistant handed this thread to a human and nobody has picked it up |
conversation.handoff_reasonrequired | string | null | Why the thread was handed to a human, when it was |
conversation.last_message_textrequired | string | null | Preview of the most recent message |
conversation.last_message_atrequired | string (date-time) | null | |
conversation.unread_countrequired | integer | Inbound 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 | null | Display name |
contact.phonerequired | string | null | E.164 phone number, when the channel has one |
{
"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.
| Name | Type | Description |
|---|---|---|
conversationrequired | object | |
conversation.idrequired | string (uuid) | UUID |
conversation.contact_idrequired | string (uuid) | UUID |
conversation.contactrequired | object | The person on the other side |
conversation.contact.idrequired | string (uuid) | UUID |
conversation.contact.namerequired | string | null | Display name |
conversation.contact.phonerequired | string | null | E.164 phone number, when the channel has one |
conversation.channelrequired | string (enum) | The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp |
conversation.connection_idrequired | string (uuid) | null | For 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) | null | Member user id, or null when unassigned |
conversation.ai_ownershiprequired | string (enum) | null | Who is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman |
conversation.needs_attentionrequired | boolean | The assistant handed this thread to a human and nobody has picked it up |
conversation.handoff_reasonrequired | string | null | Why the thread was handed to a human, when it was |
conversation.last_message_textrequired | string | null | Preview of the most recent message |
conversation.last_message_atrequired | string (date-time) | null | |
conversation.unread_countrequired | integer | Inbound 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 |
{
"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`).
| Name | Type | Description |
|---|---|---|
conversationrequired | object | |
conversation.idrequired | string (uuid) | UUID |
conversation.contact_idrequired | string (uuid) | UUID |
conversation.contactrequired | object | The person on the other side |
conversation.contact.idrequired | string (uuid) | UUID |
conversation.contact.namerequired | string | null | Display name |
conversation.contact.phonerequired | string | null | E.164 phone number, when the channel has one |
conversation.channelrequired | string (enum) | The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp |
conversation.connection_idrequired | string (uuid) | null | For 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) | null | Member user id, or null when unassigned |
conversation.ai_ownershiprequired | string (enum) | null | Who is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman |
conversation.needs_attentionrequired | boolean | The assistant handed this thread to a human and nobody has picked it up |
conversation.handoff_reasonrequired | string | null | Why the thread was handed to a human, when it was |
conversation.last_message_textrequired | string | null | Preview of the most recent message |
conversation.last_message_atrequired | string (date-time) | null | |
conversation.unread_countrequired | integer | Inbound 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) | null | null = unassigned |
{
"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.
| Name | Type | Description |
|---|---|---|
conversationrequired | object | |
conversation.idrequired | string (uuid) | UUID |
conversation.contact_idrequired | string (uuid) | UUID |
conversation.contactrequired | object | The person on the other side |
conversation.contact.idrequired | string (uuid) | UUID |
conversation.contact.namerequired | string | null | Display name |
conversation.contact.phonerequired | string | null | E.164 phone number, when the channel has one |
conversation.channelrequired | string (enum) | The channel the thread lives onwhatsappmessengerinstagramwebchatpersonal_whatsapp |
conversation.connection_idrequired | string (uuid) | null | For 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) | null | Member user id, or null when unassigned |
conversation.ai_ownershiprequired | string (enum) | null | Who is answering: the assistant, a human, or null = inherit from the channel’s assistant bindingaihuman |
conversation.needs_attentionrequired | boolean | The assistant handed this thread to a human and nobody has picked it up |
conversation.handoff_reasonrequired | string | null | Why the thread was handed to a human, when it was |
conversation.last_message_textrequired | string | null | Preview of the most recent message |
conversation.last_message_atrequired | string (date-time) | null | |
conversation.unread_countrequired | integer | Inbound 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) | null | assistantmember |
{
"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.
| Name | Type | Description |
|---|---|---|
contactrequired | object | |
contact.idrequired | string (uuid) | UUID |
contact.namerequired | string | null | Display name |
contact.phonerequired | string | null | E.164 phone number, or null for a contact reached only on Messenger/Instagram/web chat |
contact.emailrequired | string | null | Email address, when one is known |
contact.companyrequired | string | null | Company or organisation the contact belongs to |
contact.avatar_urlrequired | string | null | Profile picture URL, when a channel supplied one |
contact.tagsrequired | object[] | Tags applied to this contact |
contact.tags[].idrequired | string (uuid) | UUID |
contact.tags[].namerequired | string | Tag name |
contact.tags[].colorrequired | string | Hex 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 | string | Field name as shown in the dashboard |
contact.custom_fields[].valuerequired | string | null | The value for this contact |
contact.created_atrequired | string (date-time) | ISO 8601 timestamp |
contact.updated_atrequired | string (date-time) | ISO 8601 timestamp |
{
"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.
| Name | Type | Description |
|---|---|---|
contactrequired | object | |
contact.idrequired | string (uuid) | UUID |
contact.namerequired | string | null | Display name |
contact.phonerequired | string | null | E.164 phone number, or null for a contact reached only on Messenger/Instagram/web chat |
contact.emailrequired | string | null | Email address, when one is known |
contact.companyrequired | string | null | Company or organisation the contact belongs to |
contact.avatar_urlrequired | string | null | Profile picture URL, when a channel supplied one |
contact.tagsrequired | object[] | Tags applied to this contact |
contact.tags[].idrequired | string (uuid) | UUID |
contact.tags[].namerequired | string | Tag name |
contact.tags[].colorrequired | string | Hex 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 | string | Field name as shown in the dashboard |
contact.custom_fields[].valuerequired | string | null | The value for this contact |
contact.created_atrequired | string (date-time) | ISO 8601 timestamp |
contact.updated_atrequired | string (date-time) | ISO 8601 timestamp |
{
"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.
| Name | Type | Description |
|---|---|---|
contact_idrequired | string (uuid) | UUID |
tagrequired | object | |
tag.idrequired | string (uuid) | UUID |
tag.namerequired | string |
{
"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.
| Name | Type | Description |
|---|---|---|
contact_idrequired | string (uuid) | UUID |
tagrequired | object | |
tag.idrequired | string (uuid) | UUID |
tag.namerequired | string |
{
"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.
| Name | Type | Description |
|---|---|---|
dealrequired | object | |
deal.idrequired | string (uuid) | UUID |
deal.titlerequired | string | What the deal is for |
deal.valuerequired | number | Deal value as a number (the API coerces the decimal column) |
deal.currencyrequired | string | null | ISO 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 | null | The stage the deal currently sits in |
deal.stage.idrequired | string (uuid) | UUID |
deal.stage.namerequired | string | Stage name |
deal.contact_idrequired | string (uuid) | null | |
deal.conversation_idrequired | string (uuid) | null | |
deal.assigned_torequired | string (uuid) | null | Member user id |
deal.notesrequired | string | null | Free-text notes on the deal |
deal.expected_close_daterequired | string | null | YYYY-MM-DD |
deal.created_atrequired | string (date-time) | ISO 8601 timestamp |
deal.updated_atrequired | string (date-time) | ISO 8601 timestamp |
{
"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.
| Name | Type | Description |
|---|---|---|
dealrequired | object | |
deal.idrequired | string (uuid) | UUID |
deal.titlerequired | string | What the deal is for |
deal.valuerequired | number | Deal value as a number (the API coerces the decimal column) |
deal.currencyrequired | string | null | ISO 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 | null | The stage the deal currently sits in |
deal.stage.idrequired | string (uuid) | UUID |
deal.stage.namerequired | string | Stage name |
deal.contact_idrequired | string (uuid) | null | |
deal.conversation_idrequired | string (uuid) | null | |
deal.assigned_torequired | string (uuid) | null | Member user id |
deal.notesrequired | string | null | Free-text notes on the deal |
deal.expected_close_daterequired | string | null | YYYY-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 |
{
"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.
| Name | Type | Description |
|---|---|---|
dealrequired | object | |
deal.idrequired | string (uuid) | UUID |
deal.titlerequired | string | What the deal is for |
deal.valuerequired | number | Deal value as a number (the API coerces the decimal column) |
deal.currencyrequired | string | null | ISO 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 | null | The stage the deal currently sits in |
deal.stage.idrequired | string (uuid) | UUID |
deal.stage.namerequired | string | Stage name |
deal.contact_idrequired | string (uuid) | null | |
deal.conversation_idrequired | string (uuid) | null | |
deal.assigned_torequired | string (uuid) | null | Member user id |
deal.notesrequired | string | null | Free-text notes on the deal |
deal.expected_close_daterequired | string | null | YYYY-MM-DD |
deal.created_atrequired | string (date-time) | ISO 8601 timestamp |
deal.updated_atrequired | string (date-time) | ISO 8601 timestamp |
{
"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.
| Name | Type | Description |
|---|---|---|
dealrequired | object | |
deal.idrequired | string (uuid) | UUID |
deal.titlerequired | string | What the deal is for |
deal.valuerequired | number | Deal value as a number (the API coerces the decimal column) |
deal.currencyrequired | string | null | ISO 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 | null | The stage the deal currently sits in |
deal.stage.idrequired | string (uuid) | UUID |
deal.stage.namerequired | string | Stage name |
deal.contact_idrequired | string (uuid) | null | |
deal.conversation_idrequired | string (uuid) | null | |
deal.assigned_torequired | string (uuid) | null | Member user id |
deal.notesrequired | string | null | Free-text notes on the deal |
deal.expected_close_daterequired | string | null | YYYY-MM-DD |
deal.created_atrequired | string (date-time) | ISO 8601 timestamp |
deal.updated_atrequired | string (date-time) | ISO 8601 timestamp |
{
"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.
| Name | Type | Description |
|---|---|---|
broadcastrequired | object | |
broadcast.idrequired | string (uuid) | UUID |
broadcast.namerequired | string | Campaign name, as shown in the dashboard |
broadcast.kindrequired | string (enum) | template = an approved WhatsApp template; free_text = a personal-WhatsApp messagetemplatefree_text |
broadcast.channelrequired | string | whatsapp | personal_whatsapp |
broadcast.templaterequired | object | null | The 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) | null | When it was (or will be) started |
broadcast.countsrequired | object | Live delivery counters — poll this endpoint for progress |
broadcast.counts.totalrequired | integer | Recipients materialised |
broadcast.counts.sentrequired | integer | Handed to the channel |
broadcast.counts.deliveredrequired | integer | Confirmed delivered to the handset |
broadcast.counts.readrequired | integer | Confirmed read |
broadcast.counts.repliedrequired | integer | Recipients who replied |
broadcast.counts.failedrequired | integer | Rejected by the channel |
broadcast.created_atrequired | string (date-time) | ISO 8601 timestamp |
broadcast.updated_atrequired | string (date-time) | ISO 8601 timestamp |
{
"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.
| Name | Type | Description |
|---|---|---|
bookingrequired | object | |
booking.idrequired | string (uuid) | UUID |
booking.contact_idrequired | string (uuid) | null | The contact this booking is for |
booking.assignee_user_idrequired | string (uuid) | null | The member who owns the slot |
booking.typerequired | object | null | The booking type, when one is set |
booking.type.idrequired | string (uuid) | UUID |
booking.type.namerequired | string | |
booking.titlerequired | string | null | |
booking.locationrequired | string | null | Snapshotted 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 |
{
"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.
| Name | Type | Description |
|---|---|---|
bookingrequired | object | |
booking.idrequired | string (uuid) | UUID |
booking.contact_idrequired | string (uuid) | null | The contact this booking is for |
booking.assignee_user_idrequired | string (uuid) | null | The member who owns the slot |
booking.typerequired | object | null | The booking type, when one is set |
booking.type.idrequired | string (uuid) | UUID |
booking.type.namerequired | string | |
booking.titlerequired | string | null | |
booking.locationrequired | string | null | Snapshotted 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 |
{
"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.
| Name | Type | Description |
|---|---|---|
bookingrequired | object | |
booking.idrequired | string (uuid) | UUID |
booking.contact_idrequired | string (uuid) | null | The contact this booking is for |
booking.assignee_user_idrequired | string (uuid) | null | The member who owns the slot |
booking.typerequired | object | null | The booking type, when one is set |
booking.type.idrequired | string (uuid) | UUID |
booking.type.namerequired | string | |
booking.titlerequired | string | null | |
booking.locationrequired | string | null | Snapshotted 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 |
{
"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.
| Name | Type | Description |
|---|---|---|
inquiryrequired | object | |
inquiry.idrequired | string (uuid) | UUID |
inquiry.statusrequired | string (enum) | Where the inquiry sits in triagenewin_progressclosedspam |
inquiry.topicrequired | string | Your own category for the inquiry — free text, e.g. "Billing" |
inquiry.namerequired | string | Who sent it |
inquiry.emailrequired | string | null | Sender email, lowercased; null if the form collected only a phone |
inquiry.phonerequired | string | null | Sender phone; null if the form collected only an email |
inquiry.companyrequired | string | null | Sender company, when the form asks |
inquiry.messagerequired | string | What they wrote |
inquiry.sourcerequired | string | null | Which form this came from, e.g. "Pricing page" |
inquiry.source_pathrequired | string | null | The 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) | null | When 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 |
{
"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.
| Name | Type | Description |
|---|---|---|
inquiryrequired | object | |
inquiry.idrequired | string (uuid) | UUID |
inquiry.statusrequired | string (enum) | Where the inquiry sits in triagenewin_progressclosedspam |
inquiry.topicrequired | string | Your own category for the inquiry — free text, e.g. "Billing" |
inquiry.namerequired | string | Who sent it |
inquiry.emailrequired | string | null | Sender email, lowercased; null if the form collected only a phone |
inquiry.phonerequired | string | null | Sender phone; null if the form collected only an email |
inquiry.companyrequired | string | null | Sender company, when the form asks |
inquiry.messagerequired | string | What they wrote |
inquiry.sourcerequired | string | null | Which form this came from, e.g. "Pricing page" |
inquiry.source_pathrequired | string | null | The 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) | null | When 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) | null | newin_progressclosedspam |
statusrequired | string (enum) | Where the inquiry sits in triagenewin_progressclosedspam |
{
"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.
| Name | Type | Description |
|---|---|---|
runrequired | object | |
run.idrequired | string (uuid) | Run id |
run.assistant_idrequired | string (uuid) | The assistant running |
run.assistantrequired | object | null | The assistant, or null if it has since been deleted |
run.assistant.idrequired | string (uuid) | Assistant id |
run.assistant.namerequired | string | Assistant name |
run.assistant.colorrequired | string | null | Accent colour |
run.statusrequired | string (enum) | Where the run is in its lifecyclequeuedrunningwaiting_replywaiting_approvalwaiting_childrencompletedfailedcancelled |
run.goalrequired | string | What this run was asked to do |
run.triggerrequired | object | Where the run came from |
run.trigger.typerequired | string | One of manual, inbound_message, schedule, event, webhook or delegation |
run.trigger.trigger_idrequired | string (uuid) | null | The assistant trigger, when one fired it |
run.trigger.trigger_namerequired | string | null | That trigger name |
run.trigger.event_keyrequired | string | null | The event key, for an event trigger |
run.contact_idrequired | string (uuid) | null | The contact it concerns, if any |
run.conversation_idrequired | string (uuid) | null | The thread it may answer in, if any |
run.parent_run_idrequired | string (uuid) | null | Set when this is a sub-agent of another run |
run.root_run_idrequired | string (uuid) | null | The top of the delegation tree |
run.depthrequired | integer | 0 for a root run |
run.resultrequired | object | null | Present once the run has ended |
run.result.outcomerequired | string | null | How it ended, in one word |
run.result.summaryrequired | string | null | One line on what happened |
run.tick_countrequired | integer | Model turns this run has taken |
run.cost_usdrequired | number | Billed AI spend attributed to this run |
run.error_coderequired | string | null | Machine code when the run failed |
run.error_messagerequired | string | null | Operator-facing failure text |
run.started_atrequired | string (date-time) | When the run was created |
run.ended_atrequired | string (date-time) | null | When it reached a terminal status |
run.created_atrequired | string (date-time) | When the row was written |
{
"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.
| Name | Type | Description |
|---|---|---|
runrequired | object | |
run.idrequired | string (uuid) | Run id |
run.assistant_idrequired | string (uuid) | The assistant running |
run.assistantrequired | object | null | The assistant, or null if it has since been deleted |
run.assistant.idrequired | string (uuid) | Assistant id |
run.assistant.namerequired | string | Assistant name |
run.assistant.colorrequired | string | null | Accent colour |
run.statusrequired | string (enum) | Where the run is in its lifecyclequeuedrunningwaiting_replywaiting_approvalwaiting_childrencompletedfailedcancelled |
run.goalrequired | string | What this run was asked to do |
run.triggerrequired | object | Where the run came from |
run.trigger.typerequired | string | One of manual, inbound_message, schedule, event, webhook or delegation |
run.trigger.trigger_idrequired | string (uuid) | null | The assistant trigger, when one fired it |
run.trigger.trigger_namerequired | string | null | That trigger name |
run.trigger.event_keyrequired | string | null | The event key, for an event trigger |
run.contact_idrequired | string (uuid) | null | The contact it concerns, if any |
run.conversation_idrequired | string (uuid) | null | The thread it may answer in, if any |
run.parent_run_idrequired | string (uuid) | null | Set when this is a sub-agent of another run |
run.root_run_idrequired | string (uuid) | null | The top of the delegation tree |
run.depthrequired | integer | 0 for a root run |
run.resultrequired | object | null | Present once the run has ended |
run.result.outcomerequired | string | null | How it ended, in one word |
run.result.summaryrequired | string | null | One line on what happened |
run.tick_countrequired | integer | Model turns this run has taken |
run.cost_usdrequired | number | Billed AI spend attributed to this run |
run.error_coderequired | string | null | Machine code when the run failed |
run.error_messagerequired | string | null | Operator-facing failure text |
run.started_atrequired | string (date-time) | When the run was created |
run.ended_atrequired | string (date-time) | null | When it reached a terminal status |
run.created_atrequired | string (date-time) | When the row was written |
{
"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.
| Name | Type | Description |
|---|---|---|
askrequired | object | |
ask.idrequired | string (uuid) | Ask id |
ask.run_idrequired | string (uuid) | The run parked on this question |
ask.assistant_idrequired | string (uuid) | null | The 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 | string | What the assistant is asking |
ask.contextrequired | string | null | Background it supplied with the question |
ask.optionsrequired | object[] | For a choice: the options offered. Empty for every other kind |
ask.options[].idrequired | string | Pass this back as option_id |
ask.options[].labelrequired | string | What to show a human |
ask.options[].descriptionrequired | string | null | Extra detail, if the assistant gave any |
ask.tool_idrequired | string | null | For a tool_call: the tool the model proposed and has NOT run |
ask.tool_inputrequired | map<string, any> | null | The arguments it proposed |
ask.min_rolerequired | string | The 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 | null | The decision recorded, once answered |
ask.answerrequired | map<string, any> | null | The answer recorded |
ask.answered_byrequired | string (uuid) | null | The member who answered. Null when an API key, a timeout or a withdrawal settled it |
ask.answered_atrequired | string (date-time) | null | When it was answered |
ask.answer_sourcerequired | string (enum) | null | How it settledhumantimeoutcancel |
ask.created_atrequired | string (date-time) | When the assistant asked |
ask.updated_atrequired | string (date-time) | When it last changed |
{
"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"
}
}