Webhook event types
Subscribe to any combination of these events when configuring a
webhook. All payloads are workspace-scoped — workspace_id is either
the brand’s UUID or the __org__ sentinel for intentionally org-level
rows (the empty-string shape is no longer emitted).
Delivery events
Fired when a message’s delivery status changes. On the internal event
bus these flow as channel_delivery_status (plus dot-namespaced
per-channel variants), and the classifier marks them PASSIVE so they
never count toward MAU. The
outbound webhook flattens them to the names below.
| Event | Trigger | Key data fields |
|---|---|---|
delivery.sent | Message queued for delivery | message_id, campaign_id, contact_id, channel |
delivery.delivered | Provider confirmed delivery | message_id, campaign_id, contact_id, channel, delivered_at |
delivery.failed | Permanent delivery failure | message_id, contact_id, channel, error_code, error_reason |
delivery.bounced | Email bounced | message_id, contact_id, bounce_type (hard/soft), bounce_reason |
delivery.opened | Recipient opened the message | message_id, contact_id, channel, opened_at |
delivery.clicked | Recipient clicked a link | message_id, contact_id, channel, link_url, clicked_at |
delivery.replied | Recipient replied (WhatsApp, SMS) | message_id, contact_id, channel, reply_text |
delivery.read | Message marked as read (WhatsApp, RCS) | message_id, contact_id, channel, read_at |
Since the workspace-scoped delivery substrate rolled out (2026-05-22),
delivery webhooks carry workspace_id reliably — providers preserve
it via echoed metadata (MSG91 variables, SendGrid custom_args,
Razorpay notes, Cashfree subscription_tags), URL-encoded callback
paths (POS, shipping, Shopify), or a DB lookup fallback (Stripe,
Clerk).
Contact events
Fired when contact data changes.
| Event | Trigger | Key data fields |
|---|---|---|
contact.created | New contact added | contact_id, email, phone, source (sdk/api/import/webhook) |
contact.updated | Contact properties changed | contact_id, changes (object with old/new values per field) |
contact.deleted | Contact removed | contact_id, deletion_reason |
contact.merged | Two contacts merged | surviving_id, merged_id, merge_source |
Segment events
Fired when contacts enter or leave segments.
| Event | Trigger | Key data fields |
|---|---|---|
segment.entered | Contact matched segment rules | contact_id, segment_id, segment_name |
segment.exited | Contact stopped matching segment rules | contact_id, segment_id, segment_name |
Campaign events
Fired when campaign state changes.
| Event | Trigger | Key data fields |
|---|---|---|
campaign.started | Campaign began sending | campaign_id, campaign_name, segment_id, channel |
campaign.completed | Campaign finished sending | campaign_id, total_sent, total_delivered, total_bounced |
campaign.paused | Campaign manually paused | campaign_id, paused_by |
Billing events
Fired when billing-relevant events occur.
| Event | Trigger | Key data fields |
|---|---|---|
billing.credit_low | Credit balance dropped below threshold | current_balance, threshold, workspace_id |
billing.plan_changed | Subscription plan changed | old_plan, new_plan, effective_at |
Filtering tips
- Subscribe to
delivery.*to pipe delivery data into your analytics warehouse - Subscribe to
contact.created+contact.updatedto keep your CRM in sync - Subscribe to
billing.credit_lowto auto-top-up credits via your payment flow - Start with 2-3 event types and expand as needed — you can update subscriptions anytime
What’s next
- Signature verification — validate that payloads came from Active Reach
- Retry policy — what happens when your endpoint is down