Skip to main content

Webhooks v1 → v2

Webhooks v1 is per workspace and configured as an Action. Webhooks v2 is per organization: one endpoint, one place, every workspace.

Nothing forces you to move. Both systems are supported and run side by side. This page is for deciding whether v2 does your job better, and for doing the move if it does.

The short version

Webhooks v1Webhooks v2
Configured inEach workspace, as an ActionOrganization → Webhooks
Number of things to manageOne rule per event per bindingOne endpoint per receiver
New workspace joinsRecreate every rule thereNothing to do
VocabularyEvent Rule triggersThe activity event catalog
PayloadFive shapes by family, includes field valuesOne shape, identifiers only
Per-Form / per-Workflow targetingYes — a rule binds to one objectNo — you filter on your side
Data record eventsYesYes — emitted, gated on the per-Form opt-in
Retries5 attempts, 4xx is permanent12 attempts over ~a day, 4xx retried
Backlog while your receiver is downLost after the retriesHeld for 7 days, delivers on resume
Test sendPer rule, in the action formPer endpoint, with an editable payload

Move to v2 when

  • You manage the same integration across several workspaces, and adding a workspace means recreating a pile of rules.
  • You want a receiver outage to be recoverable rather than a gap — v2 holds the backlog for seven days and delivers it when you resume.
  • You want one place to see what is subscribed and what has been delivered.

Stay on v1 when

  • You need data record events that fire today. v2 now emits data_record.created and data_record.updated for a Form whose webhook_events_enabled opt-in is on (off by default), and not yet for records written by a connector sync. If you need record events for a connector-fed Form today, keep that part on v1; see below.
  • You need the payload to carry field values, not just identifiers. v2 delivers ids and you re-read over the API.
  • You need the event to fire only for one specific Form, Workflow or Journey, and doing that filter on your side is not acceptable.

The two differences that actually bite

There is no per-object filtering in v2

A v1 rule can be bound to one object: this Form, this Workflow, this Journey. Only that object's events fire it.

v2 has no equivalent. An endpoint subscribed to an event type receives every occurrence of that type, from every workspace it covers, and you drop the ones you do not want in your handler. The identifiers you need to make that decision are in the payload — workspace_uuid, subject, and the ref fields inside detail.

This is a real trade, and mostly a good one: instead of N rules to keep in step across M workspaces, you have one endpoint and one if in code you already control. The cost is that events you do not care about cross the wire.

Server-side per-endpoint filtering (on Form id, say) is a reasonable thing to add later. If your integration needs it, say so — it is a feature request, not a limitation of the design.

Data record events

v1's data_record:created, data_record:updated and data_record:changed are the usual foundation of a "sync this Form's submissions to our system" integration.

v2 emits data_record.created and data_record.updated for a Form that has opted in, carrying the data_type reference and the slugs of the fields that changed — never their values. Three things to know before you move onto them:

  • Emission is opt-in, per Form, and off by default. Turn on webhook_events_enabled on the Form; until you do, an endpoint subscribing to these receives nothing from it. Connector-synced records do not emit yet (the eCW/Elation bridge and Monday record-target inbound), so if a connector writes your Form's rows, keep that part of the integration on v1 for now. Records written through the REST API, the AI tools, CSV/import, signup and the enrollment /submit funnel do emit.
  • It replaces :created and :updated, not :changed. v1's :changed is a catch-all; subscribing to both v2 types is the closest equivalent.
  • There is no per-Form targeting — as with every v2 event, you filter on dataType on your side. See There is no per-object filtering in v2.

member.attributes_changed remains the type to use when what you care about is the Member's own record rather than a standalone Form submission: it carries the data_type reference and changed field slugs for the member attributes.

Mapping the vocabulary

Only some v1 triggers have a v2 counterpart, and the counterpart is often coarser, because v2 records occurrences rather than rule firings.

v1 triggerv2 event typeNotes
data_record:createddata_record.createdEmitted for a Form whose per-DataType opt-in is on; not for connector-synced records
data_record:updateddata_record.updatedEmitted for a Form whose per-DataType opt-in is on; not for connector-synced records
data_record:changeddata_record.created + data_record.updatedv1's catch-all has no single counterpart
member:updatedmember.attributes_changedField slugs only, no values
workspace:member_joinedmember.createdv1 trigger is not implemented; v2's is
workspace:member_leftmember.removedv1 trigger is not implemented; v2's is. Carries a removal reason
chat:createdchat.startedManager and agent-job chats never delivered
chat:summarychat.summarySignal only: names the Chat and channel, never the summary text
workflow:executeassignment.startedv2 names the Member's run, not the step
task:*v2 has no per-Task events
journey:enrolledjourney.enrolledSubject is the member, not the journey
journey:completedjourney.completedSubject is the member
journey_step:executejourney.step_transitionedCarries from/to status
call:startedcall.started
call:completedcall.completedCarries a disposition
call:summarycall.summaryPhone channels only; signal only, never the summary text
calendar_event:*No v2 calendar events
routine:triggeredroutine.startedv2 also has routine.completed / routine.failed
voicemail:receivedvoicemail.receivedNo transcript in the payload
fax:receivedNo v2 fax events
connector:event

The v2 event catalog is the authoritative list, generated from the vocabulary itself.

Worked example: many per-Form rules for one data type

The shape this comes up in: an integration partner needed one workspace's Intake Form kept in sync with their system, so they created a Send Webhook Action per event, each bound to that Form — data_record:created, data_record:updated, plus a few member events — and then repeated the set for every workspace that came online.

What to do with that today:

  1. Inventory what you actually have. In each workspace, go to Actions and filter to Send Webhook actions. Note, for each: the trigger, the object it is bound to, and the destination URL. The Event Rules API will list them if there are too many to read by hand.

  2. Split the list in two. Move the data_record:* rules to v2 if the Form is opted in (webhook_events_enabled) and its rows are written through the REST API, AI tools, CSV/import, signup or the enrollment /submit funnel. Leave them on v1 for now if a connector sync writes the Form's rows — connector-synced records do not emit yet (details). Everything else (member, chat, call, journey, routine, voicemail events) can move.

  3. Create one v2 endpoint at Organization → Webhooks. Leave the workspace list empty so it covers every workspace including future ones — that is the part that removes the per-workspace work. Subscribe it to the v2 types from step 2.

  4. Add the filter you used to get from the binding. Where a v1 rule was bound to the Intake Form, your handler now checks the payload instead. For member.attributes_changed the Form is detail.data_type.uuid; for other types it is whichever ref names the object. Ignore what does not match.

  5. Test before you cut over. Press Send test event on the endpoint, pick a type, and edit the sample payload so the ids are ones your handler will resolve. Nothing is queued and nothing is retried, so you can do this as many times as you need. Check your handler ran and that the X-Webhook-Signature verified.

  6. Run both for a while. Keep the v1 rules enabled until you have seen the v2 endpoint deliver real traffic in the delivery log. There is no double-processing hazard you cannot handle: dedupe your handler on v2's dedupe_key and on v1's eventId.

  7. Delete the v1 rules you replaced — but only those. Leave any data_record:* rules whose Form you did not opt in / whose rows a connector writes in place.

Net result for that partner: any data_record:* rules for connector-fed Forms remain, one per workspace, and everything else collapses into a single endpoint that covers new workspaces automatically.

Things to change in your handler

  • Verify against the raw body. Same as v1: keys arrive alphabetically sorted and the signature covers the exact bytes.
  • Dedupe on dedupe_key. v2 is at-least-once; the same value arrives on every retry and replay. v1's equivalent is eventId.
  • Return 2xx fast. v2 retries 4xx as well as 5xx, so "reject it and it goes away" is not true here. Accept, then process asynchronously.
  • Ignore unknown enum values. Every vocabulary in a v2 payload is closed but growing. An unrecognised event_type, subject.kind or detail enum member means "a new member exists", not "something is wrong".
  • Re-read for detail. v2 delivers identifiers. Where a v1 payload handed you member.email or dataRecord.fields, v2 hands you a uuid and you fetch.
  • Handle "test": true. A test send sets it at the top level of the body. Discard or route those however you like — it is inside the signed body, so you can trust it.