Field Guarantees

This page tells you which fields you can rely on being present, per event — so you can decide what to type as required vs. optional in your consumer.

The example payload on an event's reference page shows the shape of a delivery. It is not a presence contract: a field appearing in an example does not mean it is sent on every delivery. Presence is determined by the code path that emits the event, and it varies by event. The tables below are the authoritative source, verified against the emitting code (audit date: 2026-07-17).

📘

This page describes v1 (legacy) payloads — the fields as they are set when each event is created. v2 payloads apply an additional sanitization layer on top of this: even a field that is "Always present" here may be dropped from v2 if it is not on its category's allowlist. See Webhooks v2 — Overview and the v2 note at the bottom of this page.

How to read a delivery

Every event, regardless of type, carries a fixed envelope. These fields are schema-enforced and always present:

v1 (legacy)v2Notes
eventTypeevent_typeSame value in both versions (e.g. order_created)
_id / idevent_idStable across retries — safe as your idempotency key
targetEntity, targetEntityModel— (v2 exposes typed ids instead)
ownerEntity, ownerEntityModel
eventTitle— (not in v2)Free-text; do not parse
eventDescription— (not in v2)Free-text, and often absent
createdAt, updatedAttimestampNot equivalent. v1 createdAt is the event's creation time and is stable across retries. v2 timestamp is the delivery-attempt time — it is regenerated on every retry, so a retried older event can carry a newer timestamp than a fresh event. Do not use v2 timestamp to order events; v2 has no stable creation-time field today.
deleted, __v
api_versionFixed schema version, currently "2026-08-09"
affiliate_id
patient_id, patient_external_id, patient_marketing_consent, is_returning_patientv2 always enriches with these four
attribution, journey_id⚠️ Conditional independently. journey_id is present when the event has a resolved journey relationship. attribution is present only when the journey captured tracking codes. Each field is omitted when its own condition is not met. Neither is emitted on support or patient events. See Attribution Passthrough

Everything else lives in the event-specific payload — eventData (v1, nested) or the top-level snake_case fields (v2). That is what the tables below govern.

🚧

eventTitle / eventDescription are human-readable strings that can change without notice, and they do not exist in v2. Never key business logic off them — use eventType and the structured fields.

✅ Always present

For these events, the listed field(s) are set unconditionally by every emitting code path — safe to type as required (for v1).

EventGuaranteed eventData field(s)
order_createdorderId
order_expeditedperformedBy {id, role, name}
affiliate_admin_was_assignedperformedBy {id, role, name}
consultation-billed-to-affiliateamount
consultation-billed-to-patientamount
order_asset_unlockedorderAssetId, orderId, unlockedAt
patient_communication_answered_callperformedBy {id, role, name}
patient_communication_unanswered_callperformedBy {id, role, name}
patient_communication_text_message_sentperformedBy {id, role, name}
patient_communication_follow_up_requiredperformedBy {id, role, name}
pharmacy_bundle_fax_sentpharmacy
pharmacy_prescription_status_updatenewStatus
prescription_approved_by_practitionerperformedBy {id, role, name}, prescription
prescription_sent_to_pharmacypharmacy, affiliate
provider_was_assignedpractitioner
provider_was_reassignedpractitioner
visit_type_updatedvisitId, performedBy {id, role, name}
subscription_renewedsubscription_id, subscription_status

Always present when the event fires — but the event fires only conditionally

The fields are reliable, but the event itself is only emitted under a specific condition. Handle the field as required inside your handler, but don't assume the event always arrives.

EventGuaranteed field(s)Only fires when
lab_order_critical_result_receivedlabOrderId, labName, interpretation, externalOrderId, providerEventType, providerOrderStatus, latestProviderEventStatus, reportedAt, webhookPayloadA critical lab result is received
lab_order_abnormal_result_received(same as critical)An abnormal result is received and abnormal-follow-up is enabled for the affiliate
visit_unlockedorderId, visitId, visitIdsA visit is unlocked
order_ready_for_submission— (envelope + order entity fields)Affiliate settings require manual submission and the order reaches the ready state
visit_type_overriddenperformedBy {id, role, name}The project defines a visit-type override
pharmacy_processing_prescriptionpharmacy_statusPharmacy lifecycle webhook fires
pharmacy_fulfilled_prescriptionpharmacy_statusPharmacy lifecycle webhook fires
shipment_deliveredshipment_status, tracking fieldsPackage delivery is recorded

Envelope only (no event-specific fields — by design)

These deliver just the envelope; eventData is intentionally empty. There are no payload fields to rely on.

order_submitted · prescription_refill · external_questionnaire_instance_synchronization_attempt

⚠️ Conditional

For these events, some or all event-specific fields are not guaranteed — they depend on which code path fired, on optional data, or on an integration being configured. Type these as optional / nullable.

EventReliableConditional / may be absent
new_status_set_to_requestnewStatusperformedBy (only on some firings)
order_updatedaction, performedBy (most firings send empty eventData)
order_payment_collectedperformedBy (only some firings)
order_product_variation_transitionedtitrationConfig, configName, statusTransition, etc. (shape varies by firing)
case_status_updatedcaseIdexternalCaseStatus (may be undefined)
shipping_details_setshippingDetails (object always present)trackingNumber (the CTRL-pharmacy path can fire with only a trackingUrl, and the admin-set path passes the submitted details through unvalidated); shippingCompany (optional on the CTRL and admin paths); order (absent on admin-set and some transformer paths)
message-from-admin-practitioner-receivedpatientConversationId, message, type, sendersenderRole (only for known sender ids)
message-from-patient-receivedpatientConversationId, message, type, channelType, sender, senderRolesenderRole is always "patient"
order_reminder_sentorderLink, reminderTypereminderType is initial or scheduled
support_case_createdkeys always presentexternalCaseIdentifier, externalCaseStatus, externalCaseUrl may be empty strings when Jira omits them
support_case_status_updatedField set differs between status-change and assignee-change firings
lab_order_status_updatenewStatus (most paths)performedBy; lab-integration paths emit a different shape entirely
lab_order_sent_to_labFields present only via the Junction integration; the SpotLab path sends empty eventData
prescription_pharmacy_delivery_errorerror presentThe internal shape of error is a raw pass-through — not a guaranteed {message, code}
order_appointment_scheduledorderId, appointmentType, startDateTime, durationparticipants may be empty; only emitted by the appointment-scheduling integration
journey_checkout_startedjourney_id, affiliate_id, template_id, total_value_cents, currency, productstemplate_display_name, journey_type, patient_id, patient_external_id, started_at, coupon_code, attribution
journey_completed(same as checkout_started)(same as checkout_started)
journey_abandoned(same, plus recovery_url, recovery_url_expires_at)(same as checkout_started)
ecommerce_order_paidecommerce_order_id, commerce_status, total_value_cents, currencypayment_status
ecommerce_order_refundedecommerce_order_id, commerce_status, refund_amount_cents, refund_typepayment_status
subscription_cancelledsubscription_id, subscription_statustotal_value_cents, currency
subscription_pausedsubscription_id, subscription_statustotal_value_cents, currency
subscription_payment_failedsubscription_id, subscription_statusecommerce_order_id, commerce_status (not set by live renewal dead-letter emission — test-fire samples may include them)
post_purchase_disqualification_processedorder_id; ecommerce_order_id, commerce_status (v1: from targetEntity; v2: sanitizer)refund_amount_cents (successful refund branch only — absent on void and when a refund attempt fails); orchestration detail fields (branch, failed, subscription_cancelled, clinical_order_id, note_id, disqualifiers) are set in v1 eventData but stripped from v2

⛔ Not delivered

Envelope only — documented event-specific fields are not actually sent. These events fire, but their eventData is empty; do not rely on any payload fields. (In v2 you still get the envelope + patient-identity block.)

patient_enrolled · payment_declined

Never emitted — no current code path creates these. Subscribing will not reliably deliver a webhook. Do not build on them until they are implemented.

pharmacy_rejected_prescription · order_entered_waiting_room

v2 note — fields redacted relative to v1

v2 payloads run the v1 eventData through a per-category allowlist and a PHI blocklist, so a field that is "Always present" above can still be absent from v2 if it is not on its category's allowlist. The most impactful redactions for integrators:

EventPresent in v1Redacted from v2
message-from-admin-practitioner-receivedmessage, type, sender, senderRoleMessage body and sender identity — type is delivered as message_type
message-from-patient-receivedmessage, type, sender, senderRoleMessage body and sender identity — type is delivered as message_type; channelType is delivered as channel_type
order_payment_collected, affiliate_admin_was_assigned, visit_type_updated, visit_type_overriddenperformedByperformedBy (blocked as PHI-adjacent)
provider_was_assignedpractitionerpractitioner
post_purchase_disqualification_processedorder_id, ecommerce_order_id, commerce_status (target entity), refund_amount_cents (successful refund branch only), branch, failed, subscription_cancelled, clinical_order_id, note_id, disqualifiersOrchestration detail — v2 delivers commerce/order identifiers and refund_amount_cents when applicable

Fields that are delivered in v2 (contrary to older documentation):

Eventv2 fields now delivered
shipping_details_set, shipment_deliveredtracking_number, tracking_url, shipping_carrier, shipment_status
prescription_pharmacy_delivery_errorSanitized reason (internal error strings are replaced with safe defaults)
pharmacy_prescription_status_updatepharmacy_status (newStatus aliases to pharmacy_status, not order_status)
support_case_created / support_case_status_updatedsupport_case_id, support_case_status, support_case_name, support_case_category, external case fields
order_asset_unlockedorder_asset_id, unlocked_at, order_external_identifier, asset_link
order_reminder_sentorder_link, reminder_type

If your integration needs any field listed in the first table, keep the affected webhook on v1. The full allowlist per category is on the v2 Overview.


This table reflects a full audit of the emitting code paths as of 2026-07-17. If you find a delivery that contradicts it, please flag it — a mismatch is a bug on our side, not expected behavior.