Webhooks v2 (organization)
One endpoint, configured once for the whole organization, that receives a chosen set of event types from across the organization's workspaces. No rule to create in each workspace, and no rule to remember when a workspace is added.
This is a different system from Webhooks v1, which is per-workspace and configured as an Action. They share the signing scheme and the egress-safety rules and nothing else: v2 has its own event vocabulary, its own endpoint model, its own payload and its own delivery guarantees. Running both at once is fine and normal.
Already on v1? Webhooks v1 → v2 covers what maps across, what does not yet, and what to do about it.
Configure endpoints at Organization → Webhooks.
What you get
| Scope | The whole organization, or a named subset of its workspaces |
| Vocabulary | A closed set of activity event types — see the catalog |
| Payload | One shape for every event type. Identifiers only |
| Delivery | A durable queue: at-least-once, retried for up to a day, held for 7 days |
| Signing | HMAC-SHA256, same scheme as v1 — see Request signing |
| Testing | A Send test event button on every endpoint, with an editable payload |
Setting one up
- Go to Organization → Webhooks and create an endpoint: a name, an
HTTPS URL, and the event types it subscribes to. Production rejects
http://and internal hosts — the same rule as v1. - Choose which workspaces it covers. Leave the workspace list empty and it covers every workspace in the organization, including ones created later — this is the default and the common case. Link specific workspaces to narrow delivery to exactly those.
- Save the signing secret. It is generated for you unless you supply your own,
and it is returned exactly once, in the create response. Every later read
shows a masked placeholder (
__SECRET_SET__); sending that placeholder back on update preserves the stored secret, and sending any other value rotates it. - Press Send test event and check your handler ran. See Testing below.
Which workspaces an endpoint covers is a link list, not a mode. There is no exclude mode: "every workspace except the test ones" is an include list naming the ones you do want, and it has to be updated by hand when a workspace should join it.
Event types are drawn from the closed vocabulary below, narrowed to the types deliverable at workspace visibility — an org-only event (billing, domain verification) is rejected when you configure it, rather than accepted and then silently never sent.
Who can do what
| Read endpoints, the vocabulary, the delivery log | org:webhooks — org admins hold it automatically |
| Create, update, delete, send a test | Org admin, with the 2FA step-up |
| See which org endpoints your workspace feeds | webhooks:read inside that workspace |
Every create, update and delete of an endpoint's configuration is written to the admin-config audit log. Status changes (pause, resume, the system's own auto-disable) live in the endpoint's status history. A test send is recorded separately again — it changes no configuration, but it is an outbound request our infrastructure made on an operator's say-so.
Payload
One shape for every event type: the raw activity event envelope, plus which endpoint delivered it. There are no per-family variants to learn.
It carries identifiers only — no member names, no message content, no
DataRecord field values. Resolve what you need through the API using the ids
in subject, refs and detail.
{
"id": "6c87845e-73e3-538a-9e9b-a197023cf884",
"occurred_at": "2026-01-15T14:22:00Z",
"event_type": "member.created",
"event_version": 1,
"visibility": "workspace",
"workspace_uuid": "5099b28c-b8f0-5d8f-9ea9-2d077cb89af3",
"org_uuid": "3c8c25ca-c126-52f7-8a1b-3a40ba93c609",
"account_uuid": null,
"actor": { "kind": "system", "uuid": null },
"subject": {
"kind": "member",
"uuid": "5ec1da83-d1c9-5aa7-849c-a6fcb0d18631",
"workspace_uuid": "5099b28c-b8f0-5d8f-9ea9-2d077cb89af3",
"org_uuid": null,
"account_uuid": null
},
"refs": [],
"detail": {
"schema_version": 1,
"backfill": false,
"type": "member.created",
"origin": "api",
"connection": null,
"account": null,
"capabilities": ["members:read"]
},
"dedupe_discriminator": null,
"dedupe_key": "member.created:member:5ec1da83-d1c9-5aa7-849c-a6fcb0d18631",
"endpoint": {
"uuid": "9c1f7e5a-1b2c-4d3e-8f90-a1b2c3d4e5f6",
"name": "Acme CRM sync"
}
}
| Field | What it is |
|---|---|
id | This occurrence's own uuid |
occurred_at | When it happened, UTC, timezone-aware |
event_type | One of the catalog values |
event_version | Envelope version. Bumped only if the envelope's own shape changes |
visibility | Always workspace on a delivered event |
workspace_uuid | The workspace the event rolled up under |
org_uuid | Always the delivering endpoint's organization, whatever the underlying event recorded |
account_uuid | The account, when the occurrence names one |
actor | Who caused it: {kind, uuid}, uuid null exactly when kind is system |
subject | The entity the event is about, as a typed reference |
refs | Other entities the occurrence names, same shape as subject |
detail | The typed payload — varies by event type, see the catalog |
dedupe_key | The idempotency key. Same value on every retry. Dedupe on it |
endpoint | Which of your endpoints delivered this |
Every reference — subject, each of refs, and the ref-shaped fields inside
detail — is {kind, uuid, workspace_uuid, org_uuid, account_uuid}. Never an
inline name, never a value.
detail always carries type (equal to the envelope's event_type),
schema_version, and backfill (true when the row was synthesized from
canonical data by a backfill job rather than observed live). The catalog below
lists what each type adds.
event_type, visibility, subject.kind and every enum inside detail come
from closed sets. Treat an unrecognised value as "ignore, don't error" —
these vocabularies grow.
What never arrives
- Account-private occurrences. An event reaches an endpoint only by the workspace it rolls up under. Chat events are workspace occurrences, direct messages included — a DM is your team talking to a Member. The exception is a Member's own tool sessions (a manager chat, an agent job chat), which roll up under that account rather than a workspace, so they are never delivered here even when the endpoint's workspaces would otherwise include them.
- Org-only events. Billing and domain-verification types exist in the vocabulary but are not deliverable at workspace visibility, so they cannot be subscribed to.
- Delivery outcomes. A failed delivery is not itself an event. There is no
webhook.failedin the vocabulary and there never will be; a delivery outcome cannot be subscribed to. - Field-level detail. Raising deliveries to the field-level detail v1
payloads carry (
include_payload) is not available yet.
Delivery
| Method | POST |
Content-Type | application/json |
| Body | JSON with alphabetically sorted keys, UTF-8 |
| Signature header | X-Webhook-Signature — HMAC-SHA256, see Request signing |
| Timeout | 30 seconds total, connection included |
| Redirects | Not followed |
| Success | Any 2xx |
| Retries | Up to 12 attempts, exponential backoff from 60 seconds, doubling to a 1-hour cap |
Deliveries run on a fixed once-a-minute schedule rather than firing the instant the event happens. This is a durable queue, not a live push: a burst of activity is spread across ticks rather than delivered all at once.
4xx is retried here. Unlike v1, no HTTP status is by itself a permanent
rejection — timeouts, connection errors, 3xx, 4xx and 5xx are all retried
on the same schedule. Return 2xx as soon as you have durably accepted the
payload, and do your processing asynchronously.
At-least-once, not exactly-once. dedupe_key is the same value on every
retry and on any replay. Dedupe your handler on it.
Two failures skip the retry schedule and dead-letter immediately, because retrying cannot fix either: a destination that fails the egress-safety check (the hostname is resolved and the connection pinned to the address actually vetted), and a missing or corrupted signing secret.
When your receiver stays broken
A delivery dead-letters after 12 attempts. After 5 dead-lettered deliveries in 24 hours, the endpoint is automatically disabled and no new sends start until an operator resumes it; the org UI explains why from the endpoint's status history.
Nothing is lost while paused or disabled. New events keep queueing and the
backlog delivers once the endpoint is active again — for up to 7 days. A
queued delivery older than that expires, shows in the delivery log as expired,
and never counts toward the auto-disable threshold.
An endpoint's status is active, paused (an operator's choice) or disabled
(the circuit breaker). Every change, including the initial active, records who
made it (null when the system did), why, and when.
Deleting an endpoint stops delivery immediately, but its already-queued deliveries are not deleted with it — they simply age out under the same 7-day retention.
Delivery and attempt rows are an operational troubleshooting log kept for 30 days, not the compliance record. Configuration changes stay in the admin-config audit log and status transitions stay in the status history after delivery details age out.
Testing
Every endpoint has a Send test event action (⋯ menu on the endpoint card). Pick an event type, and you get the real payload for it, filled with example identifiers. Edit anything you like — put in an id your handler will actually resolve, drop a field to see what your parser does without it — and send. You get back the HTTP status, how long it took, and your own server's reply body.
The test goes through exactly the same code a real delivery does: the same egress vetting, the same address pinning, the same canonicalization, the same HMAC, the same outcome classification. If your receiver accepts the test, it accepts the real thing.
What it deliberately does not do:
- It queues nothing. No delivery row, no attempt, no retry.
- It cannot disable your endpoint. Test against a receiver that 500s twenty times and the endpoint's failure budget is untouched.
- It does not fill your delivery log, so the log stays useful for diagnosing real traffic.
It works on a paused or disabled endpoint too — those states hold the queue
back, and "is it fixed yet?" is exactly the question you have while an endpoint
is paused.
The delivered body carries "test": true at the top level. It is in the
body rather than a header on purpose: the signature covers the body, so a
receiver that wants to discard test traffic can key on a value it has verified.
The event-type picker lists the types this endpoint is subscribed to, with find-as-you-type narrowing. To try a handler against a type before you commit to the subscription, subscribe to it, test, then unsubscribe — the test send itself never changes the subscription.
The direct test API is wider than the UI picker: POST .../test still accepts
any event type in the closed subscribable vocabulary, subscribed or not. The
picker narrows the offer; the API keeps the contract.
API
| Route | What it does |
|---|---|
GET/POST /api/v2/o/{org_uuid}/webhooks | List and create endpoints |
GET/PATCH/DELETE /api/v2/o/{org_uuid}/webhooks/{endpoint_uuid} | Read, update, delete one |
GET /api/v2/o/{org_uuid}/webhooks/event-types | The closed subscribable vocabulary |
GET /api/v2/o/{org_uuid}/webhooks/event-types/{event_type}/sample | A representative payload for one type |
POST /api/v2/o/{org_uuid}/webhooks/{endpoint_uuid}/test | Send one payload now; returns the outcome |
GET /api/v2/o/{org_uuid}/webhooks/{endpoint_uuid}/deliveries | 30-day delivery log for one endpoint |
GET /api/v2/w/{workspace_uuid}/webhook-logs/org-endpoints | From inside a workspace: which org endpoints it feeds |
Notes on the shapes:
workspaceUuidsis the endpoint's current sorted link list.limitedToWorkspacesdistinguishes a limited endpoint whose last workspace was deleted from whole-org coverage. On update, omitting the field leaves the links unchanged; sending[]switches the endpoint to every workspace.- Each endpoint carries
statusHistory, most recent first, up to 20 entries. - Endpoint reads return a destination fingerprint (
scheme://hostplus a short digest of the full URL) rather than the URL — webhook paths and queries routinely carry per-tenant tokens. The full URL is returned only in the create response, like the one-time secret. - The delivery log pairs each delivery (event type, when it occurred, its dedupe key, its workspace, queue status, when it next retries) with every attempt made at it, oldest first (attempt number, start and finish, outcome, failure class and error type, HTTP status).
- The workspace-side read shows name, destination fingerprint, status, event
types and whether the endpoint is limited — never the signing secret, never
delivery contents. It is available to any member holding
webhooks:read, not only admins.
Event catalog
data_record.created and data_record.updated are emitted (GRA-8876), but
only for a Form that has opted in. The opt-in is the per-DataType
webhook_events_enabled setting, off by default: a record write in a Form
that has not opted in produces no event at all — not a row that is later
filtered — so an endpoint subscribed to these types receives nothing from that
Form. Turn it on per Form before relying on record events.
Which surfaces emit, today. Records written through the REST API, the AI
data tools, CSV/import, signup, and the enrollment /submit funnel all reach
one shared post-commit emitter, so an opted-in Form's writes from those surfaces
publish. Connector-synced records do not emit yet: the eCW/Elation sync
bridge and Monday record-target inbound write records without reaching that
post-commit step, and the enrollment /track path and the capture-only inquiry
path write funnel telemetry that deliberately dispatches nothing. Those gaps are
follow-up work — do not rely on data_record.* for a Form whose rows a
connector writes until they land.
The payload is IDs and field slugs, never field values: subject names the
data_record, detail.data_type names the Form, and detail.field_slugs lists
the slugs that were written or changed. A record's values are where its PHI
lives, so a consumer that needs contents reads the record back through the API
under its own authorization. If you still need the v1 per-workspace record rules
for payload contents, see
v1's data-record events.
chat.summary and call.summary are emitted, and they carry no
content. They signal that a summary was persisted: chat.summary for a Chat
on any channel, call.summary for a phone-channel Chat only — the historical
v1 call:summary contract. The envelope names the Chat (chat.summary) or the
call (the Chat's most recent PhoneCall, for call.summary) as subject, and
the detail carries the channel plus, for the call type, the Chat reference
when its identifier is a UUID. The summary text is PHI and is deliberately
never in the payload; a receiver that needs it reads the Chat back through the
API under its own authorization. Both types are unconditional — there is no
per-Chat opt-in.
There are 42 subscribable event types, in six families. Every one delivers the envelope described above; what changes between them is subject and detail.
People
Who is in a workspace and what is recorded about them — the member lifecycle, role and label changes, attribute edits, and the imports that do all of those in bulk.
member.created
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
origin | MemberOrigin | no |
connection | entity ref | no |
account | entity ref | no |
capabilities | list of ActivityCapability | no |
member.removed
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
reason | MemberRemovalReason | yes |
account | entity ref | no |
member.role_changed
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
account | entity ref | no |
capabilities | list of ActivityCapability | no |
member.labels_changed
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
added | list of entity ref | no |
removed | list of entity ref | no |
member.attributes_changed
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
field_slugs | list of string | no |
data_type | entity ref | no |
import.started
subject is a import reference.
detail field | Type | Always present |
|---|---|---|
source | ImportSource | yes |
connection | entity ref | no |
expected_rows | integer | no |
import.completed
subject is a import reference.
detail field | Type | Always present |
|---|---|---|
source | ImportSource | yes |
connection | entity ref | no |
created_count | integer | no |
updated_count | integer | no |
skipped_count | integer | no |
failed_count | integer | no |
duration_ms | integer | no |
data_record.created
subject is a data_record reference.
detail field | Type | Always present |
|---|---|---|
field_slugs | list of string | no |
data_type | entity ref | no |
data_record.updated
subject is a data_record reference.
detail field | Type | Always present |
|---|---|---|
field_slugs | list of string | no |
data_type | entity ref | no |
Conversations
Every exchange with a person, in any channel: chats, escalations, calls, voicemail, and a Member's run through a Workflow (an Assignment).
assignment.started
subject is a assignment reference.
detail field | Type | Always present |
|---|---|---|
workflow | entity ref | no |
revision_number | integer | no |
trigger | AssignmentTrigger | no |
assignment.concluded
subject is a assignment reference.
detail field | Type | Always present |
|---|---|---|
workflow | entity ref | no |
outcome | AssignmentConclusion | yes |
task_count | integer | no |
completed_task_count | integer | no |
duration_ms | integer | no |
chat.started
subject is a chat reference.
detail field | Type | Always present |
|---|---|---|
lane | ChatLane | yes |
channel | ChatChannel | yes |
initiated_by | ActorKind | yes |
chat.awaiting_human
subject is a chat reference.
detail field | Type | Always present |
|---|---|---|
reasons | list of ChatAttentionReason | yes |
cleared_reasons | list of ChatAttentionReason | no |
chat.human_replied
subject is a chat reference.
detail field | Type | Always present |
|---|---|---|
cleared_reasons | list of ChatAttentionReason | no |
waited_ms | integer | no |
reply_message_uuid | uuid | no |
reply_member | entity ref | no |
chat.call_ended
subject is a chat reference.
detail field | Type | Always present |
|---|---|---|
reasons | list of ChatAttentionReason | no |
cleared_reasons | list of ChatAttentionReason | no |
chat.summary
subject is a chat reference.
detail field | Type | Always present |
|---|---|---|
channel | ChatChannel | yes |
escalation.opened
subject is a escalation reference.
detail field | Type | Always present |
|---|---|---|
reason | EscalationReason | yes |
chat | entity ref | no |
escalation.resolved
subject is a escalation reference.
detail field | Type | Always present |
|---|---|---|
resolution | EscalationResolution | yes |
open_ms | integer | no |
chat | entity ref | no |
call.started
subject is a call reference.
detail field | Type | Always present |
|---|---|---|
direction | CallDirection | yes |
phone_number | entity ref | no |
call.completed
subject is a call reference.
detail field | Type | Always present |
|---|---|---|
direction | CallDirection | yes |
outcome | CallCompletionDisposition | yes |
duration_ms | integer | no |
recorded | boolean | no |
call.missed
subject is a call reference.
detail field | Type | Always present |
|---|---|---|
direction | CallDirection | yes |
reason | MissedCallReason | yes |
call.summary
subject is a call reference.
detail field | Type | Always present |
|---|---|---|
channel | ChatChannel | yes |
chat | entity ref | no |
voicemail.received
subject is a voicemail reference.
detail field | Type | Always present |
|---|---|---|
duration_ms | integer | no |
phone_number | entity ref | no |
transcribed | boolean | no |
voicemail.listened
subject is a voicemail reference.
detail field | Type | Always present |
|---|---|---|
waited_ms | integer | no |
Journeys
A Member's progress through a Journey. Four of the five name the member as their subject; only journey.activated names the Journey.
journey.enrolled
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
journey | entity ref | yes |
trigger | JourneyEnrollmentTrigger | no |
journey.unenrolled
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
journey | entity ref | yes |
reason | JourneyExitReason | no |
journey.completed
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
journey | entity ref | yes |
step_count | integer | no |
duration_ms | integer | no |
journey.step_transitioned
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
journey | entity ref | yes |
step | entity ref | yes |
transition | JourneyTransition | yes |
from_status | JourneyStepStatus | no |
to_status | JourneyStepStatus | yes |
journey.activated
subject is a journey reference.
detail field | Type | Always present |
|---|---|---|
step_count | integer | no |
Automation
The machine working on its own, with nobody on the other end: Routine runs and a Workflow being published.
routine.started
subject is a routine reference.
detail field | Type | Always present |
|---|---|---|
routine | entity ref | yes |
trigger | RoutineTrigger | yes |
routine.completed
subject is a routine reference.
detail field | Type | Always present |
|---|---|---|
routine | entity ref | yes |
outcome | RoutineOutcome | yes |
duration_ms | integer | no |
routine.failed
subject is a routine reference.
detail field | Type | Always present |
|---|---|---|
routine | entity ref | yes |
failure | RoutineFailure | yes |
duration_ms | integer | no |
workflow.published
subject is a workflow reference.
detail field | Type | Always present |
|---|---|---|
revision | entity ref | no |
revision_number | integer | yes |
task_count | integer | no |
Operators
Who is available and who is in which operator group.
presence.went_online
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
device | PresenceDevice | no |
operator_groups | list of entity ref | no |
live_workspaces | list of entity ref | no |
presence.went_offline
subject is a member reference.
detail field | Type | Always present |
|---|---|---|
device | PresenceDevice | no |
online_duration_ms | integer | yes |
reason | PresenceOfflineReason | yes |
operator_group.member_added
subject is a operator_group reference.
detail field | Type | Always present |
|---|---|---|
group | entity ref | yes |
member | entity ref | yes |
operator_group.member_removed
subject is a operator_group reference.
detail field | Type | Always present |
|---|---|---|
group | entity ref | yes |
member | entity ref | yes |
Administration
The account around the work: phone numbers, domains, settings, subscriptions and purchases.
member_subscription.started
subject is a member_subscription reference.
detail field | Type | Always present |
|---|---|---|
plan_slug | string | yes |
interval | BillingInterval | yes |
member_subscription.canceled
subject is a member_subscription reference.
detail field | Type | Always present |
|---|---|---|
plan_slug | string | no |
reason | SubscriptionCancelReason | yes |
phone_number.provisioned
subject is a phone_number reference.
detail field | Type | Always present |
|---|---|---|
number_kind | PhoneNumberKind | no |
country | string | no |
capabilities | list of PhoneCapability | no |
phone_number.archived
subject is a phone_number reference.
detail field | Type | Always present |
|---|---|---|
reason | PhoneNumberArchiveReason | yes |
config.updated
subject is a workspace_config reference.
detail field | Type | Always present |
|---|---|---|
area | ConfigArea | yes |
action | ConfigAction | yes |
field_slugs | list of string | no |
timezone_before | string | no |
timezone_after | string | no |
Closed vocabularies
Every value below comes from a closed set that grows by review. Treat a value you do not recognise as "ignore, do not error" — a new member is an additive change and will not be announced as a breaking one.
ActivityCapability
assignments:read, chats:read, journeys:read, members:read, operator:read, org:read, phones:read, records:read, workflows:read, workspace:read
ActorKind
account, agent, connection, member, org_account, system
AssignmentConclusion
success, failed, referred, caller_hangup, member_unsubscribe, admin_canceled, expired, system_error, busy, no_answer
AssignmentTrigger
api, journey, manual, rule, schedule
BillingInterval
month, quarter, year, once
CallCompletionDisposition
completed, busy, no-answer, failed, canceled
CallDirection
inbound, outbound
ChatAttentionReason
agent_handed_off, escalation_open, legacy_unattributed, posture_awaits_human, unanswered_inbound
ChatChannel
email, internal, sms, voice, web, whatsapp
ChatLane
agent_job, dm, manager, workspace
ConfigAction
created, deleted, updated
ConfigArea
business_hours, escalation_settings, inbox, phone_number, workspace_settings
EscalationReason
agent_requested, member_requested, policy, tool_approval
EscalationResolution
answered, approved, declined, expired
ImportSource
api, connector_sync, csv
JourneyEnrollmentTrigger
api, import, manual, rule
JourneyExitReason
completed, manual, member_removed, rule
JourneyStepStatus
pending, eligible, in_progress, completed, skipped, failed, timed_out
JourneyTransition
activated, advanced, failed, skipped
MemberOrigin
api, connector, import, manual, signup, system, org_api_key
MemberRemovalReason
admin, deleted, merged, retention
MissedCallReason
after_hours, no_answer, no_operator_online, rejected, timeout
PhoneCapability
fax, mms, sms, voice
PhoneNumberArchiveReason
admin, ported_out, released
PhoneNumberKind
local, mobile, short_code, toll-free
PresenceDevice
browser, phone, sip, unknown
PresenceOfflineReason
disconnect, manual, timeout
RoutineFailure
canceled, error, timeout
RoutineOutcome
no_action, partial, succeeded
RoutineTrigger
api, manual, rule, schedule
SubscriptionCancelReason
admin, migration, payment_failure, replaced, requested
Related
- Webhooks v1 — per-workspace webhooks configured as Actions
- Webhooks v1 → v2 — what maps across, and what does not
- Request signing — the HMAC scheme both versions share