Webhooks v2 — Overview

What changed in v2

Webhooks v2 introduces two independent improvements:

Featurev1 (legacy)v2
Payload versioningFull raw event objects, including PHISanitized envelopes with per-category allowlists
Signature headersha256={base64_hmac} over body onlyt={unix_ts},sha256={base64_hmac} over t.body — replay protection included
api_version fieldNot present"2026-08-09" (Journey events: "2026-08-06")

Existing webhooks default to v1. Newly created webhooks default to v2.


Payload versioning

Every v2 payload carries a fixed api_version field:

{
  "event_type": "order_created",
  "event_id": "event::abc123",
  "api_version": "2026-08-09",
  "timestamp": "2026-05-30T14:00:00.000Z",
  "affiliate_id": "aff::xyz",
  "order_id": "order::abc"
}

The api_version value for most v2 envelopes is 2026-08-09. It identifies the payload schema and will be bumped if a breaking change is ever made. Journey-native events are the exception and use "2026-08-06" (see Versioning guarantees).

⚠️

timestamp is the delivery-attempt time, not the event's creation time — it is regenerated on every retry. Don't use it to order or dedupe events; use event_id for dedup and reconcile state per entity for ordering (see Delivery & Retries).

PHI sanitization — per-category allowlists

PHI is removed from v2 payloads at dispatch time. Each event category has an explicit allowlist governing which eventData fields pass through; any field not on the list is stripped before delivery. patient_id does not appear on any category's allowlist below — but every v2 payload, regardless of category, also carries four patient-identity fields (patient_id, patient_external_id, patient_marketing_consent, is_returning_patient) added by a separate, always-on enrichment step that isn't governed by the category allowlist at all.

CategoryAllowed fields
ORDERorder_id, order_status, case_status, case_id, ecommerce_order_id, journey_id, commerce_status, product_variation_ids, product_display_names, subscription_status, total_value_cents, currency, order_tag, patient_conversation_id, channel_type, visit_type, visit_id, external_case_identifier, external_case_status, order_asset_id, unlocked_at, order_external_identifier, asset_link, titration_config_id, titration_direction, config_name, status_transition, questionnaire_id, location_id, previous_product_variation_id, current_product_variation_ids, future_product_variation_ids, current_pv_count, future_pv_count, future_prescriptions_to_create, video_requested, reason, message_type, order_link, reminder_type
PRESCRIPTIONprescription_id, prescription_status, pharmacy_id, order_id, commerce_status, product_variation_ids, product_display_names, visit_id, video_requested, reason
SHIPPINGorder_id, prescription_id, shipment_status, commerce_status, tracking_number, tracking_url, shipping_carrier
PHARMACYprescription_id, pharmacy_id, pharmacy_status, order_id, commerce_status, reason
PAYMENTorder_id, total_value_cents, currency, payment_status, consultation_id
LABlab_order_id, lab_order_status, order_id, commerce_status, interpretation, lab_name, external_order_id, provider_order_status, reported_at, provider_event_type, latest_provider_event_status
SUPPORTsupport_case_id, support_case_status, support_case_name, support_case_category, order_id, external_case_identifier, external_case_status, external_case_url
JOURNEYjourney_id, template_id, template_display_name, journey_type, ecommerce_order_id, started_at, completed_at, abandoned_at, total_value_cents, currency, products, coupon_code, recovery_url, recovery_url_expires_at, commerce_status, product_variation_ids, product_display_names, subscription_status, attribution
COMMERCEecommerce_order_id, subscription_id, subscription_status, commerce_status, total_value_cents, currency, payment_status, product_variation_ids, product_display_names, journey_id, order_id, refund_amount_cents, refund_type
PATIENT(empty — no eventData fields exposed beyond the universal patient-identity fields above)
INTERNAL(empty — no eventData fields exposed beyond the universal patient-identity fields above)

There is no SUBSCRIPTION or CHANNEL category — subscription fields (subscription_id, subscription_status) are exposed under COMMERCE and ORDER/JOURNEY instead.

Beyond the allowlist, a separate blocklist strips known-PHI keys (email, phone, firstName/lastName, dateOfBirth, address/city/state/zip, medications, allergies, diagnosis, clinicalNotes, credentials/tokens, and others) as defense in depth if they ever appear on an allowlisted path.

Field coverage on v2 payloads

The allowlist above only keeps an eventData field if its name matches an allowlist entry (directly, or via a small alias table). Events introduced alongside the v2 payload system (Journey, Commerce/subscription, Payment events) use field names that match their category's allowlist, so their v2 payloads carry full detail. Patient events (patient_enrolled) carry full detail too, but for a different reason — their entire meaningful payload is the universal patient-identity fields described above, not anything from the (empty) PATIENT category allowlist.

Most events now include their event-specific fields directly in v2 payloads. Where detail is not included in the payload itself, it is retrievable through the authenticated API using the entity IDs the payload delivers — v2 payloads are designed as notifications, not data transfers. Status reasons and order-history details are retrievable through dedicated endpoints without requiring v1 payloads.


Migration path

  1. Create a new webhook or use the upgrade endpoint to switch an existing webhook to v2 payload delivery.
  2. Update your consumer to verify the new signature format (see Signature Verification).
  3. Remove any code that expects PHI fields — they will not appear in v2 payloads.
  4. If you need to roll back from v2 to v1, use the rollback endpoint (requires master token).

Event dictionary

The webhook dictionary defines 59 event types, of which 56 are publicly subscribable by affiliates. Two are internal-only (never delivered to affiliate webhooks), and one additional public event (pharmacy_rejected_prescription) is defined but not yet implemented — it is excluded from subscription (56 + 2 + 1 = 59). All 56 subscribable events are emitted by live code paths. To retrieve the full list programmatically:

GET /webhooks/actions/getDictionary

Note this endpoint lives on the legacy v1 webhook management API (/webhooks/...), not the /v2/webhooks/... module — it predates payload versioning and works the same regardless of which payload version your webhook is configured for. See the getDictionary endpoint for the response shape.

For a complete per-event breakdown of which fields are guaranteed, conditional, or not delivered, see Field Guarantees.

For narrative documentation of individual events (purpose, example payload), see the event pages under each category: Order, Prescription, Pharmacy, Lab, Payment, Journey, Commerce, Patient, and Support Events.


Versioning guarantees

  • Most v2 envelopes use api_version: "2026-08-09". This replaced "2026-05-24" when marketing attribution was extended beyond journey events — see Attribution Passthrough. The change is additive: consumers that ignore unknown fields need no action.
  • Exception — Journey-native events (journey_checkout_started, journey_completed, journey_abandoned) always use api_version: "2026-08-06", including when the optional attribution key is omitted. See Attribution Passthrough.
  • Within a given api_version, fields are additive (never removed or renamed). Adding a significant new block such as attribution may still bump api_version so documentation and consumers can target the schema explicitly — as with the Journey bump to "2026-08-06".
  • Breaking changes (field removal, rename, type change) always bump api_version and are announced in advance.