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-08-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-17"
—timestamp_epochInteger Unix seconds for timestamp. Regenerated on retry with timestamp.
—affiliate_id
—patient_id, patient_external_id, patient_marketing_consent, is_returning_patientv2 always enriches with the two booleans (patient_marketing_consent, is_returning_patient; default false). patient_id and patient_external_id are omitted when unavailable — never sent as null. patient_id is present when the event has a linked patient; patient_external_id is present only when that patient also has an external identifier. Affiliate-owned events with no linked patient omit both ids. is_returning_patient is per-patient: another ecommerce order already in paid, processing, or completed.
—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, ecommerce_order_id, first_purchase (false), cycle_number, plan_type, payment_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_updated—action, performedBy (most firings send empty eventData)
order_payment_collected—performedBy (only some firings)
order_product_variation_transitioned—titrationConfig, 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
order_createdorderIdorderLink is sent only while the order's intake is still incomplete, and omitted once it is done
order_visit_required—orderLink present whenever the follow-up visit link could be generated
support_case_createdkeys always presentexternalCaseIdentifier, externalCaseStatus, externalCaseUrl may be empty strings when Jira omits them
support_case_status_updated—Field 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_lab—Fields 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, products, first_purchase, plan_type, event_id, timestamp_epochtemplate_display_name, journey_type, patient_id, patient_external_id, started_at, coupon_code, ecommerce_order_id, attribution, intake_form_instance_ids (affiliate intake form instance ids on the journey; omitted when none)
journey_completed(same as checkout_started)(same as checkout_started)
journey_abandonedjourney_id, affiliate_id, template_id, total_value_cents, currency, products, plan_type, event_id, timestamp_epoch, had_cart, recovery_status, recovery_missing_detailstemplate_display_name, journey_type, patient_id, patient_external_id, started_at, coupon_code, ecommerce_order_id, first_purchase (only when had_cart is true), attribution, recovery_url / recovery_url_expires_at when a link can be issued, intake_form_instance_ids (omitted when none)
journey_contact_capturedjourney_id, affiliate_id, template_id, event_id, timestamp_epoch, patient_idtemplate_display_name, journey_type, patient_external_id, started_at, attribution, intake_form_instance_ids (omitted when none). No money fields, no first_purchase, no raw email/phone. patient_id is always present because this event fires only after the journey is linked to a patient; patient_external_id is omitted when the patient has no external identifier.
custom_intake_form_completedaffiliate_id, intake_form_id, intake_form_instance_id, completed_at, responses, patient_marketing_consent, event_id, timestamp_epochjourney_id, patient_id, patient_external_id — omitted when absent (pre-login completion is not emitted until the patient is linked, so delivered payloads normally include patient_id). No template/money fields, no is_returning_patient. One event per intake form instance; preview instances never emit. See Custom Intake Form Completed.
ecommerce_order_paidecommerce_order_id, commerce_status, total_value_cents, currency, first_purchase, plan_typepayment_status, attribution, journey_id
ecommerce_order_refundedecommerce_order_id, commerce_status, total_value_cents, currency, refund_amount_cents, refund_typepayment_status, attribution, journey_id, first_purchase, plan_type
subscription_cancelledsubscription_id, subscription_statustotal_value_cents, currency, attribution, journey_id, first_purchase, plan_type
subscription_pausedsubscription_id, subscription_statustotal_value_cents, currency, attribution, journey_id, first_purchase, plan_type
subscription_payment_failedsubscription_id, subscription_statusecommerce_order_id, commerce_status (not set by live renewal dead-letter emission — test-fire samples may include them); attribution, journey_id, first_purchase, plan_type
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
order_created (intake outstanding), order_visit_requiredorder_link — the patient's intake/visit URL, expiring 7 days after it is issued
journey_abandonedhad_cart, recovery_status, recovery_missing_details (no longer stripped)
ecommerce_order_paid, subscription_renewed; ecommerce_order_refunded, subscription_cancelled, subscription_paused, subscription_payment_failed when a checkout order is resolved; journey_checkout_started, journey_completed; journey_abandoned when had_cart is true; ORDER events when an ecommerce order is resolvedfirst_purchase, plan_type. Renewals also send cycle_number. Not on journey_contact_captured, and not on PRESCRIPTION, SHIPPING, PHARMACY, LAB, PAYMENT-only, SUPPORT, or PATIENT events.
journey_contact_captured, journey_checkout_started, journey_completed, journey_abandoned; COMMERCE events; ORDER events; PRESCRIPTION, SHIPPING, PHARMACY, PAYMENT, and LAB eventsattribution with optional landing_url / referrer_url on first/last touch — omitted when no tracking data was captured. SUPPORT, PATIENT, and INTERNAL do not receive it. See Attribution Passthrough
All v2 envelopestimestamp_epoch

products[].display_name and product_display_names are removed in api_version: "2026-08-17".

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-08-17. If you find a delivery that contradicts it, please flag it — a mismatch is a bug on our side, not expected behavior.