Skip to main content

Webhook payloads

The exact body of every webhook event. Use the table to jump to a payload; each section below shows the full JSON.

For how to subscribe, verify signatures, and handle retries, see Webhooks v1.

Every payload​

Every body carries the same envelope — event, eventId, eventRule (id, uuid, name), and workspace (uuid, name) — and then adds the blocks below. Blocks are omitted, not nulled, when out of scope, and keys arrive alphabetically sorted (verify signatures against the raw request body). On lean shapes member.email is the account email; member:updated and data_record:* report the member-record email (normally the same).

EventBody
data_record:createdData record
data_record:updatedData record
data_record:changedData record
member:updatedMember updated
workspace:member_joinedMembership — ⚠️ not implemented
workspace:member_leftMembership — ⚠️ not implemented
chat:createdChat created
chat:summaryChat summary
workflow:user_messageMessage
workflow:assistant_messageMessage
workflow:executeWorkflow and task
task:executeWorkflow and task
task:enterWorkflow and task
task:commandWorkflow and task
task:exitWorkflow and task
calendar_event:createdCalendar
calendar_event:updatedCalendar
calendar_event:cancelledCalendar
calendar_event:startingCalendar
calendar_event:endedCalendar
journey:enrolledJourney
journey:completedJourney
journey_step:executeJourney
call:startedCall
call:completedCall
call:summaryCall summary
routine:triggeredRoutine
connector:eventConnector
voicemail:receivedVoicemail
fax:receivedFax

Data record​

data_record:created, data_record:updated, data_record:changed. The changed event fires for either operation and adds a top-level operation (created or updated). dataRecord.fields mirrors the form's submitted values keyed by field slug. member is the record owner; author is who triggered the event.

{
"event": "data_record:created",
"eventId": "1c9f6a2e-7d41-4f4b-9a53-8b1c2f6d0e77",
"dataType": {
"uuid": "5f3c…",
"type": "Contact Form",
"slug": "contact_form"
},
"member": {
"id": 123,
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921",
"labels": ["vip", "active"],
"createdAt": "2026-01-10T09:00:00+00:00",
"updatedAt": "2026-06-16T14:30:00+00:00"
},
"dataRecord": {
"id": 4567,
"externalId": null,
"fields": {
"message": "Hello!",
"priority": "high"
},
"createdAt": "2026-06-16T14:30:00+00:00",
"updatedAt": "2026-06-16T14:30:00+00:00"
},
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Notify CRM on new contact" },
"author": {
"id": 5,
"accountUuid": "c4d5…",
"name": "Acme Operator",
"email": "ops@acme.example"
},
"workspace": { "uuid": "9e8d…", "name": "Acme Health" }
}

For data_record:changed, the envelope begins with "operation": "updated", and an updated record also carries a change context block.

Member updated​

member:updated. Fires for any profile change, including label changes — inspect context.changed_fields to see which. member.labels is the current label slug set.

{
"event": "member:updated",
"eventId": "5b7e9d13-2a86-49c0-b7f2-64a0c9e3d215",
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"description": null,
"phone": "+15551234567",
"email": "jane@example.com",
"externalId": "crm-9921",
"labels": ["vip", "active"],
"role": "patient",
"createdAt": "2026-01-10T09:00:00+00:00",
"updatedAt": "2026-06-16T14:30:00+00:00"
},
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Sync label changes" },
"author": {
"id": 5,
"accountUuid": "c4d5…",
"name": "Acme Operator",
"email": "ops@acme.example"
},
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"context": {
"changed_fields": ["phone"],
"change_source": "user",
"fieldsUpdated": ["phone"],
"oldValues": { "phone": "+15555550100" },
"newValues": { "phone": "+15555550123" },
"timestamp": "2026-06-16T14:30:00+00:00",
"old_phone": "+15555550100",
"new_phone": "+15555550123"
}
}

Membership​

⚠️ Not yet implemented — these events do not fire. They can be selected and saved, but no code path emits them yet, so no webhook is ever delivered. Do not build against them; to detect new members, poll the Members API or use a data_record:* rule on your intake form.

workspace:member_joined, workspace:member_left. Envelope plus the member.

{
"event": "workspace:member_joined",
"eventId": "3a1f8c20-9d7e-4b16-9c02-5e8a1f0b7d64",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Sync new members to CRM" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
}
}

Chat created​

chat:created. Envelope plus the member and the chat identifiers.

{
"event": "chat:created",
"eventId": "0b4d6e18-3c72-4a90-8f15-9d2c7b6e0a53",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Log new conversations" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
},
"chat": { "id": 73219, "uuid": "f7a9…" }
}

Chat summary​

chat:summary. The chat-scoped summary. Fires after the conversation is summarized; carries the summary text and summaryUpdatedAt. It re-fires with a newer summaryUpdatedAt as the conversation continues — key on chat.id.

{
"event": "chat:summary",
"eventId": "e2c9a7b1-5f38-4d02-9a64-1b7c3e5d80f2",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Export conversation summaries" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
},
"chat": { "id": 73219, "uuid": "f7a9…" },
"summary": "AI-generated summary of the conversation. Treat as PHI.",
"summaryUpdatedAt": "2026-06-16T14:35:00+00:00"
}

Message​

workflow:user_message, workflow:assistant_message. Envelope plus member and chat. The message text is not included — use chat.id to fetch it over the authenticated API.

workflow:user_message (shown in the UI as "Member Message") delivers on every workflow-chat inference turn (any channel), and also for operator-queued and AI-suspended (human-handled) SMS/WhatsApp chats, where the rule runs without a turn: reply-authoring actions are excluded there, but webhook:send still runs. See the caveat in Webhooks.

{
"event": "workflow:user_message",
"eventId": "a7f0c3d9-4e21-4b58-9c17-6d2e8b0a5f31",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Notify on inbound reply" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
},
"chat": { "id": 73219, "uuid": "f7a9…" }
}

Workflow and task​

workflow:execute, task:execute, task:enter, task:command, task:exit. Envelope plus member and chat. Same shape as Message; only event differs.

{
"event": "task:execute",
"eventId": "c8b2e5a0-1d64-4f39-9b70-3a6c9e1d7f04",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Fire on task step" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
},
"chat": { "id": 73219, "uuid": "f7a9…" }
}

Calendar​

calendar_event:created, calendar_event:updated, calendar_event:cancelled, calendar_event:starting, calendar_event:ended. Envelope plus member. The calendar event itself is used for the rule's conditions but is not included in the payload.

{
"event": "calendar_event:created",
"eventId": "9f3d1c7a-6b28-4e05-8a94-2d7b0f6c3e18",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Sync appointments" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
}
}

Journey​

journey:enrolled, journey:completed, journey_step:execute. Envelope plus member. There is no chat block — a journey has no conversation in scope.

{
"event": "journey:enrolled",
"eventId": "4d0a7e93-2c15-4b86-9f31-8b6c0d2e5a79",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Notify on journey enrollment" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
}
}

Call​

call:started, call:completed. Envelope plus member, chat, and a call block. call:completed also carries scheduledCallbacks when the AI scheduled open follow-ups. status is a carrier-style disposition (completed, busy, no-answer, failed, canceled); answeredBy distinguishes a live answer from voicemail.

{
"event": "call:completed",
"eventId": "9d3f1c88-6b0a-45e2-9f41-c7a2b5d80e63",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Export call to data warehouse" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
},
"chat": { "id": 73219, "uuid": "f7a9…" },
"call": {
"id": 5512,
"providerCallId": "CAe0f3…",
"status": "completed",
"direction": "outbound",
"durationSeconds": 62,
"startedAt": "2026-06-16T14:29:00+00:00",
"endedAt": "2026-06-16T14:30:02+00:00",
"answeredBy": "human"
},
"scheduledCallbacks": [
{
"uuid": "b2c3…",
"scheduledTime": "2026-06-17T17:00:00+00:00",
"targetChannel": "phone-voice",
"status": "pending",
"reason": "follow up on appointment",
"scheduledBy": "agent"
}
]
}

call fields: providerCallId is the provider's call SID (null if none was assigned); status is a carrier-style disposition (nullable); direction is inbound/outbound; durationSeconds is connected time (0 for busy/no-answer, null while live); startedAt/endedAt are ISO-8601 (null until reached); answeredBy is the answering-machine-detection result or null. scheduledCallbacks lists still-open AI-scheduled callbacks ordered by scheduledTime; reason is the member's own words — treat it as PHI: store it encrypted at rest, keep it out of your logs, and return it only to authorized users.

The recording is never in the payload — fetch audio and transcript with chat.id over the authenticated API. call:started carries the same shape with status: "in-progress" and no scheduledCallbacks.

Call summary​

call:summary. The Call shape plus the summary text and summaryUpdatedAt. This is a separate event from chat:summary — voice summaries arrive here.

{
"event": "call:summary",
"eventId": "2f7b9c04-8a13-4e6d-9b52-0c3a7e1d6f85",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Export call summaries" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
},
"chat": { "id": 73219, "uuid": "f7a9…" },
"summary": "AI-generated summary of the call. Treat as PHI.",
"summaryUpdatedAt": "2026-06-16T14:31:00+00:00",
"call": {
"id": 5512,
"providerCallId": "CAe0f3…",
"status": "completed",
"direction": "outbound",
"durationSeconds": 62,
"startedAt": "2026-06-16T14:29:00+00:00",
"endedAt": "2026-06-16T14:30:02+00:00",
"answeredBy": "human"
}
}

Routine​

routine:triggered. Fires once per target member when a routine runs. Envelope plus member. There is no chat block — a routine has no conversation in scope at trigger time. The outreach's identity comes through eventRule.name.

{
"event": "routine:triggered",
"eventId": "6c1e8a3f-0d95-4b72-9e48-7a2c5b0f9d61",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "6-week check-in" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
}
}

Connector​

connector:event. Envelope plus member.

{
"event": "connector:event",
"eventId": "8e0b2d47-5c19-4a83-9f60-1d7a3e6c0b52",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Handle connector event" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"member": {
"id": 123,
"accountUuid": "c4d5…",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"externalId": "crm-9921"
}
}

Voicemail​

voicemail:received. Workspace-global: no member and no chat block — a voicemail links to a phone number and a call, not an authenticated person. Fires once after transcription resolves.

{
"event": "voicemail:received",
"eventId": "7f2b4c19-8e3d-4a55-b1c0-2d9e6f4a8b31",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "Route voicemails to the queue" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"voicemail": {
"uuid": "3c5d…",
"callerNumber": "+15555550123",
"callerName": "Sample Caller",
"recordingDuration": 23,
"transcriptionStatus": "COMPLETED",
"transcript": "Synthetic transcript for documentation only.",
"phoneNumberId": 77,
"targetMemberId": null,
"status": "NEW",
"createdAt": "2026-06-16T14:04:00+00:00",
"updatedAt": "2026-06-16T14:05:30+00:00"
}
}

callerNumber / callerName are spoofable caller ID — an unverified claim, not identity. transcript is null when transcriptionStatus is FAILED. The audio is never in the payload — fetch it over the authenticated voicemail API using voicemail.uuid.

targetMemberId is not PHI: an internal Member id, never a name or a number. It is the Member the caller was trying to reach — their primary contact — and is null when the caller has no primary contact, best-effort attribution fails, or the voicemail predates the field.

Fax​

fax:received. Workspace-global: no member and no chat block — a fax sender is never authenticated. Fires once after the document is durably stored.

{
"event": "fax:received",
"eventId": "5a8e1d72-4b96-4c03-9f27-e6b3a0d51c48",
"eventRule": { "id": 88, "uuid": "a1b2…", "name": "File inbound faxes" },
"workspace": { "uuid": "9e8d…", "name": "Acme Health" },
"fax": {
"uuid": "8b7a…",
"source": "documo",
"status": "RECEIVED",
"pageCount": 3,
"pdfFileId": 90211,
"tiffFileId": 90212,
"senderNumber": "+15555550188",
"senderCsid": "ACME CLINIC",
"connectionId": 14,
"phoneNumberId": null,
"failureCode": null,
"receivedAt": "2026-06-16T14:02:11+00:00",
"createdAt": "2026-06-16T14:02:14+00:00"
}
}

senderNumber / senderCsid are unauthenticated claims by the sending machine — never use them for authorization, routing to a patient record, or anything else that treats them as identity. source tells you which ingress received it: a fax-provider connection (connectionId set) or a fax-capable phone number (phoneNumberId set) — the other is null. failureCode is non-null when the fax arrived partially or failed mid-receipt; check it before treating pageCount as complete. The document is never in the payload — fetch the PDF over the authenticated Files API using pdfFileId.

The fax inbox owner (the member your workspace configured on the receiving endpoint) is deliberately not in the payload, so a recipient can't mistake them for the sender. If you need the owner, read memberId from the delivery's webhook log.


Change context​

Appended to member:updated, data_record:updated, and data_record:changed (when the operation was updated). Values are the member's or record's own field values — treat as PHI.

{
"context": {
"changed_fields": ["phone"],
"change_source": "user",
"fieldsUpdated": ["phone"],
"oldValues": { "phone": "+15555550100" },
"newValues": { "phone": "+15555550123" },
"timestamp": "2026-01-15T17:05:00+00:00",
"old_phone": "+15555550100",
"new_phone": "+15555550123"
}
}
  • changed_fields — changed field names, as stored (snake_case).
  • change_source — one of user, system, api, webhook, or unknown. member:updated reports user; data-record events currently report unknown.
  • fieldsUpdated — the same names, camelCased (redundant with changed_fields; both are sent for backwards compatibility).
  • oldValues / newValues — keyed by the camelCased field name.
  • timestamp — when the payload was built, not when the change happened.
  • old_<field> / new_<field> — one pair per changed field, keyed by the as-stored (snake_case) name.

Prefer oldValues / newValues for new integrations. The flattened old_* / new_* pairs and fieldsUpdated are legacy shapes retained for existing consumers.