Skip to main content

Referral Tier Integration Guide

๐Ÿ“„ This guide is also available as a PDF download.

Version: 0.1 (Draft) Date: March 2, 2026 Base URL: https://api.stealth.health (production) | https://sandbox.stealth.health (development) Status: Proposal / Scoping

See also: Clinical Partner API Integration Guide โ€” a deeper integration where patient data, intake responses, appointment details, and transaction history are shared with the partner under a full BAA.

1. Overviewโ€‹

Stealth Health offers a white-label telemedicine integration that allows partner platforms to embed our enrollment, clinical review, and prescription fulfillment pipeline into their own product. The integration works as follows:

  1. Partner directs their customer to a co-branded Stealth Health enrollment page.
  2. Patient completes a medical intake questionnaire and (optionally) pays.
  3. Stealth Health doctors review the intake and approve or deny a prescription.
  4. Stealth Health pharmacy partners fulfill and ship approved prescriptions.
  5. Partner receives real-time status updates via webhooks and can query the API for current state.

The partner never handles, stores, or receives protected health information (PHI). All clinical data stays within the Stealth Health platform. Partners interact exclusively with de-identified references and lifecycle status events.


2. HIPAA Compliance & PHI Boundariesโ€‹

What Partners CAN Accessโ€‹

Data PointExample
Partner-scoped reference IDref_abc123xyz
Enrollment statusenrolled, pending_review, approved, denied
Fulfillment statusawaiting_shipment, shipped, delivered
Timestamp of each status change2026-03-02T14:30:00Z
Product category enrolled fortrt-cream, peptides
Payment statuspayment_due, payment_complete
Tracking carrier (no tracking #)USPS, UPS, Canada Post

What Partners CANNOT Accessโ€‹

Data PointReason
Patient name, DOB, genderPHI โ€” identity
Address, phone, emailPHI โ€” contact info
Medical conditions, symptoms, allergiesPHI โ€” clinical
Prescription details (medication, dosage, quantity)PHI โ€” treatment
Doctor notes, intake responsesPHI โ€” clinical
Lab results, ID verification imagesPHI โ€” clinical / PII
Tracking numbers, shipping labelsCould be used to correlate PHI

Note: If a partner needs to display fulfillment tracking to their end user, they should direct the user to their Stealth Health patient portal, or we can discuss a tokenized tracking link approach (see Section 6.6).

Business Associate Agreement (BAA)โ€‹

A signed BAA between Stealth Health and the partner is required before API credentials are issued. The BAA defines each party's obligations under HIPAA and limits the partner's role to a referral source โ€” not a covered entity or business associate handling PHI.


3. Authentication & Securityโ€‹

3.1 API Keysโ€‹

Each partner receives a pair of API credentials:

CredentialPurpose
X-Partner-IDIdentifies the partner (public, safe to log)
X-Api-KeyAuthenticates the request (secret, never log or expose client-side)

Both must be sent as HTTP headers on every request:

GET /partner/referrals HTTP/1.1
Host: api.stealth.health (or sandbox.stealth.health)
X-Partner-ID: ptr_acme_health
X-Api-Key: sk_live_7f3a...redacted
Content-Type: application/json

3.2 Key Rotationโ€‹

Partners can request key rotation at any time. When a new key is issued, the old key remains valid for 72 hours to allow migration. Both keys are accepted during the overlap window.

3.3 Webhook Signature Verificationโ€‹

All webhook payloads include an X-Stealth-Signature header containing an HMAC-SHA256 signature of the request body, signed with the partner's webhook secret (provided at onboarding, separate from the API key).

X-Stealth-Signature: sha256=a1b2c3d4e5f6...

Partners must verify this signature before processing any webhook payload. See Section 7 for implementation details.

3.4 Transport Securityโ€‹

  • All API traffic must use TLS 1.2+.
  • IP allowlisting is available on request.
  • All requests are logged with partner ID, endpoint, timestamp, and response code (no PHI is logged).

4. Partner Onboardingโ€‹

Step 1: BAA Executionโ€‹

Sign the Business Associate Agreement.

Step 2: Configurationโ€‹

Stealth Health provisions the following for each partner:

ItemDescription
partner_idUnique partner identifier (e.g. ptr_acme_health)
API key pairLive and sandbox credentials
Webhook secretFor verifying webhook signatures
Enrollment slug(s)Co-branded enrollment URLs returned per referral (e.g. https://app.stealth.health/enroll/trt-cream?ref=ref_abc&wl=acme)
Branding configLogo, colors, and copy for co-branded enrollment pages
Webhook URLPartner's HTTPS endpoint for receiving events

Step 3: Enrollment Page Setupโ€‹

Stealth Health creates partner-specific enrollment pages based on the product categories the partner wants to offer. These pages:

  • Use the partner's branding (logo, color scheme, custom copy).
  • Include the appropriate medical intake questionnaire.
  • Are accessible via partner-specific URLs or can be embedded via iframe.
  • Automatically tag all submissions with the partner's partner_id.

Step 4: Sandbox Testingโ€‹

Partners test the full lifecycle in sandbox before going live.

Step 5: Go Liveโ€‹

Switch to production credentials and enrollment URLs.


5. Integration Flowโ€‹

5.1 Sequence Diagramโ€‹

Partner Integration Sequence Diagram

5.2 Step-by-Stepโ€‹

Step 1 โ€” Create Referral Partner calls POST /partner/referrals with a partner_reference (the partner's own customer ID) and the desired product_category. Stealth Health returns a referral_id and a unique enrollment URL.

Step 2 โ€” Customer Enrollment Partner redirects or links their customer to the enrollment URL. The customer sees a co-branded page and completes the medical intake questionnaire.

Step 3 โ€” Payment If payment is required for the product category, the customer pays during enrollment. Some categories are consult-only (payment after approval).

Step 4 โ€” Clinical Review A licensed Stealth Health physician reviews the intake and either approves a prescription or denies the request (with a reason communicated to the patient, not the partner).

Step 5 โ€” Fulfillment Approved prescriptions are sent to a licensed pharmacy partner for compounding and shipping. Partners receive status updates at each stage.


6. API Referenceโ€‹

All endpoints are prefixed with /partner.

Looking for the PHI-bearing endpoint families? Patients, appointments, intake responses, messages, transactions, lab orders & requisitions (GET /partner/patients/:id/lab-orders, GET /partner/lab-orders/:id[/requisition|/results]), patient documents, partner-submitted prescriptions/intake, and the payment modes (hosted / headless / embedded_checkout / authorize_only) are clinical-tier surfaces and are documented in the Clinical Partner API Integration Guide (lab orders: ยง 12; documents: ยง 12A; wearables data: ยง 12B). They are not available to referral-tier keys.

6.1 POST /partner/referralsโ€‹

Create a new patient referral. Call this before sending a customer to enrollment.

Request:

{
"partner_reference": "cust_12345",
"product_category": "trt-cream",
"metadata": {
"campaign": "spring-2026",
"source": "website"
}
}
FieldTypeRequiredDescription
partner_referencestringYesPartner's own identifier for this customer. Must be unique per partner. Max 128 chars.
product_categorystringYesOne of the available categories (see 6.5).
metadataobjectNoArbitrary key-value pairs for partner's own tracking (max 20 keys, 500 chars per value). Not used by Stealth Health.

Response (201 Created):

{
"referral_id": "ref_abc123xyz",
"partner_reference": "cust_12345",
"product_category": "trt-cream",
"enrollment_url": "https://app.stealth.health/enroll/trt-cream?ref=ref_abc123xyz&wl=acme",
"status": "created",
"created_at": "2026-03-02T14:00:00Z",
"expires_at": "2026-03-09T14:00:00Z"
}

Note: Enrollment URLs expire after 7 days. A new referral must be created if the link expires.


6.2 GET /partner/referrals/:referral_idโ€‹

Retrieve the current status of a referral.

Response (200 OK):

{
"referral_id": "ref_abc123xyz",
"partner_reference": "cust_12345",
"product_category": "trt-cream",
"status": "approved",
"status_history": [
{ "status": "created", "at": "2026-03-02T14:00:00Z" },
{ "status": "enrolled", "at": "2026-03-02T14:15:00Z" },
{ "status": "pending_review", "at": "2026-03-02T14:15:00Z" },
{ "status": "approved", "at": "2026-03-03T09:30:00Z" }
],
"fulfillment": {
"status": "shipped",
"carrier": "USPS",
"estimated_delivery": "2026-03-07",
"updated_at": "2026-03-04T11:00:00Z"
},
"payment": {
"status": "paid",
"amount_cents": 14900,
"currency": "USD",
"paid_at": "2026-03-02T14:15:00Z"
},
"metadata": {
"campaign": "spring-2026",
"source": "website"
},
"created_at": "2026-03-02T14:00:00Z"
}

6.3 GET /partner/referralsโ€‹

List all referrals for the authenticated partner.

Query Parameters:

ParamTypeDefaultDescription
statusstring(all)Filter by status: created, enrolled, pending_review, approved, denied, expired
fulfillment_statusstring(all)Filter by fulfillment: awaiting_shipment, shipped, in_transit, delivered
partner_referencestringโ€”Look up by partner's own reference
product_categorystring(all)Filter by category
created_afterISO 8601โ€”Only referrals created after this timestamp
created_beforeISO 8601โ€”Only referrals created before this timestamp
limitinteger50Max results per page (1โ€“200)
cursorstringโ€”Pagination cursor from previous response

Response (200 OK):

{
"referrals": [
{ "referral_id": "ref_abc123xyz", "partner_reference": "cust_12345", "product_category": "trt-cream", "status": "approved", "..." : "..." },
{ "referral_id": "ref_def456uvw", "partner_reference": "cust_67890", "product_category": "peptides", "status": "enrolled", "..." : "..." }
],
"pagination": {
"has_more": true,
"next_cursor": "eyJjcmVhdGVkX2F0Ijo..."
}
}

6.4 GET /partner/referrals/summaryโ€‹

Aggregate counts for dashboard / reporting.

Query Parameters:

ParamTypeDescription
periodstringday, week, month, all_time (default: month)
product_categorystringFilter by category

Response (200 OK):

{
"period": "month",
"start": "2026-02-01T00:00:00Z",
"end": "2026-02-28T23:59:59Z",
"totals": {
"created": 150,
"enrolled": 120,
"pending_review": 15,
"approved": 90,
"denied": 12,
"expired": 3,
"shipped": 75,
"delivered": 60
},
"by_category": {
"trt-cream": { "created": 80, "approved": 50, "denied": 5 },
"peptides": { "created": 40, "approved": 30, "denied": 4 }
},
"revenue": {
"total_cents": 756000,
"currency": "USD"
}
}

6.5 GET /partner/productsโ€‹

List product categories available to this partner.

Response (200 OK):

{
"products": [
{
"category": "trt-cream",
"display_name": "Testosterone Replacement Therapy (Cream)",
"description": "Compounded testosterone cream for hormone optimization",
"jurisdictions": ["US"],
"enrollment_url_template": "https://app.stealth.health/enroll/trt-cream?ref={referral_id}&wl=acme",
"requires_payment_at_enrollment": true,
"price_range": {
"min_cents": 9900,
"max_cents": 19900,
"currency": "USD"
}
},
{
"category": "peptides",
"display_name": "BPC-157 Peptide Therapy",
"description": "BPC-157 peptide for tissue repair and recovery support",
"jurisdictions": ["US"],
"enrollment_url_template": "https://app.stealth.health/enroll/peptides?ref={referral_id}&wl=acme",
"requires_payment_at_enrollment": true,
"price_range": {
"min_cents": 14900,
"max_cents": 24900,
"currency": "USD"
}
}
]
}

6.5a GET /partner/store/productsโ€‹

Return the white label's merged store catalog โ€” the global active store_products plus this white label's store_product_overrides (hidden products removed, re-ranked, and white-label-only products included). It mirrors exactly what the patient storefront would render, so a partner can drive their own checkout UI off the same SKUs. Wholesale and clinician-tier fields (costInCents, clinicUsePriceInCents, b2bPriceInCents) are never exposed.

Gated by the store feature flag (production). Sandbox is open to every partner. Returns 403 STORE_ACCESS_REQUIRED in production when the flag is off.

Response (200 OK):

{
"products": [
{
"sku": "magnesium-glycinate",
"name": "Magnesium Glycinate",
"description": "Highly absorbable magnesium for sleep and recovery.",
"category": "supplements",
"categories": ["supplements", "sleep"],
"image": "https://cdn.stealth.health/store/mag-glycinate.png",
"price_cents": 2900,
"currency": "USD",
"requires_consult": false,
"badge": "Best seller"
}
]
}

sku is the stable Firestore document id โ€” use it as the line-item key when pushing an order.


6.5b POST /partner/store/ordersโ€‹

Push an OTC store order. The server re-prices every line from the catalog (sku + quantity only โ€” a partner-supplied price is never trusted), applies the storefront's shipping rule (per-item rates charged once per sub-store group; $35.00 default rate per group), and routes payment per-order on payment.mode:

payment.modeBehaviour
customer (default)Mints a hosted payment link (a store_order paymentTransactions doc). The end customer pays; nothing is charged to the partner. Returns the link.
white_label_accountCharges the white label's card on file through the active OTC processor (Authorize.net or Stripe, per config/payments.otcProcessor) and fulfils the order synchronously (store_orders + ShipStation + emails). Requires a card vaulted via the reporting portal.

Hosted checkout redirect (customer mode) โ€” the "Proceed to checkout" integration. This is how a white label runs its own storefront UI on top of Stealth fulfilment: when the customer presses Proceed to checkout in the partner's store, the partner's backend POSTs the cart's SKUs to this endpoint and redirects the customer to the returned payment_link:

https://{your-portal-domain}/transaction/{transaction_id}

(e.g. https://app.puramass.com/transaction/aBc123...). The page renders the Stealth-hosted checkout on the white label's own portal host with the cart prefilled server-side from the order's re-priced line items โ€” the customer enters shipping/payment there and the order is fulfilled like any storefront purchase. This works for every white label and for the Stealth default host (https://app.stealth.health/transaction/{id}). Links minted before this path existed used /enroll/transaction/{transaction_id}; that path renders the identical page and remains permanently supported. Payment links expire after 7 days.

Tracking payment status. Two mechanisms, usable together:

  1. Webhook (push, recommended): when the customer completes payment, a store_order.payment_complete event POSTs to your registered webhook URL, carrying the transaction_id, your partner_reference, the paid line items, and the customer + shipping blocks (the ship-to address the customer entered on the hosted checkout, or the one you supplied at order creation).
  2. Polling: GET /partner/store/orders/:transaction_id returns the order's current status (payment_pending / paid / expired / cancelled) plus the same customer + shipping blocks at any time.

Tracking fulfillment/shipping status. Once the order is paid (either mode), poll GET /partner/store/orders/:id/shipping with the order_id (white_label_account mode) or transaction_id (customer mode) to pull carrier, tracking number, and delivery state into your own system on a cron.

Pricing tier (payment.pricing, optional):

payment.pricingBehaviour
retail (default)The storefront retail price โ€” the white label's own per-product price override (set in the My White Label Studio) wins over the global catalog price, exactly as the end-customer storefront charges.
clinic_useThe clinician/B2B tier: the product's B2B price when one exists (your own per-product B2B override, set in the My White Label Studio, wins over the global one), falling back to the platform wholesale (clinic-use) price, then retail. Only valid with payment.mode: "white_label_account" (the white label is buying stock on its own card). White-label RETAIL overrides are ignored on this tier โ€” you are never billed your own customer-facing markup.

If your agreement bills your card-on-file orders at clinic-use pricing, pass "pricing": "clinic_use" on every white_label_account order โ€” omitting it charges your account's default tier, which is retail unless Stealth has set your account default to clinic-use.

Account-level default (features.store_default_pricing, set by a Stealth admin): when your account default is clinic_use, every white_label_account order that omits payment.pricing bills at the clinic-use tier automatically. Customer-paid (payment.mode: "customer") orders are unaffected โ€” end customers always pay retail. An explicit per-request payment.pricing always wins over the account default, so you can still place the occasional retail-tier restock by passing "pricing": "retail". Contact partners@stealth.health to change your account default.

Request:

{
"items": [
{ "sku": "magnesium-glycinate", "quantity": 2 },
{ "sku": "vitamin-d3-k2", "quantity": 1 }
],
"customer": {
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"phone": "+15555550123"
},
"shipping": {
"address": "123 Main St",
"address2": "Apt 4",
"city": "Austin",
"state": "TX",
"zip": "78701",
"country": "US"
},
"currency": "usd",
"partner_reference": "po_2026_0042",
"payment": { "mode": "customer" }
}

A white-label-paid restock order at clinic-use pricing:

{
"items": [{ "sku": "nxgen-pen-2", "quantity": 1 }],
"customer": { "email": "orders@partner.com" },
"shipping": { "address": "...", "city": "...", "state": "...", "zip": "...", "country": "US" },
"partner_reference": "po_2026_0043",
"payment": { "mode": "white_label_account", "pricing": "clinic_use" }
}
  • items[] accepts sku (preferred), product_id, or id; quantity is clamped to 1โ€“99.
  • customer.email is required.
  • partner_reference (โ‰ค120 chars) is echoed back and used as the idempotency seed for white_label_account charges โ€” reuse the same value to safely retry without double-charging.

Response โ€” customer mode (201 Created):

{
"order": {
"status": "payment_pending",
"payment_mode": "customer",
"pricing": "retail",
"transaction_id": "aBc123...",
"payment_link": "https://app.acme.com/transaction/aBc123...",
"partner_reference": "po_2026_0042",
"currency": "usd",
"subtotal_cents": 8700,
"items": [
{ "sku": "magnesium-glycinate", "name": "Magnesium Glycinate", "quantity": 2, "unit_price_cents": 2900 }
]
}
}

Response โ€” white_label_account mode (201 Created):

{
"order": {
"status": "confirmed",
"payment_mode": "white_label_account",
"pricing": "clinic_use",
"order_id": "ord_...",
"partner_reference": "po_2026_0042",
"processor": "authorize_net",
"transaction_ref": "60123456789",
"currency": "usd",
"subtotal_cents": 8700,
"shipping_cents": 0,
"total_cents": 8700,
"items": [
{ "sku": "magnesium-glycinate", "name": "Magnesium Glycinate", "quantity": 2, "unit_price_cents": 2900 }
]
}
}

Errors:

StatusCodeMeaning
400STORE_ORDER_INVALIDMissing items or customer.email.
400STORE_PAYMENT_MODE_INVALIDpayment.mode not customer/white_label_account.
400STORE_PRICING_INVALIDpayment.pricing not retail/clinic_use, or clinic_use requested on a customer-mode order.
400STORE_PRODUCT_UNAVAILABLENone of the requested SKUs are in this white label's store.
402WHITE_LABEL_CARD_DECLINEDThe card on file was declined.
409WHITE_LABEL_BILLING_NOT_CONFIGUREDNo card on file โ€” add one in the reporting portal.
403STORE_ACCESS_REQUIREDProduction access without the store feature flag.
502STORE_CHARGE_FAILEDUpstream billing/charge error.

The white label's card on file is captured and managed in the stealth-reporting admin portal (White Labels โ†’ Card on file). It is tokenised in the browser (Accept.js or Stripe Elements) and vaulted in the active processor; only a masked summary is ever stored.


6.5c GET /partner/store/orders/:transaction_idโ€‹

Poll the payment status of a customer-mode store order minted by POST /partner/store/orders. The transaction_id is the one returned in the create response. Only your own orders resolve โ€” a transaction_id belonging to another partner (or to an organic storefront purchase) returns 404.

For push notification instead of polling, register a webhook URL and consume the store_order.payment_complete event.

Response (200 OK):

{
"order": {
"transaction_id": "aBc123...",
"status": "paid",
"payment_mode": "customer",
"partner_reference": "po_2026_0042",
"currency": "usd",
"subtotal_cents": 8700,
"payment_link": "https://app.acme.com/transaction/aBc123...",
"created_at": "2026-08-05T14:00:00.000Z",
"paid_at": "2026-08-05T14:12:31.000Z",
"expires_at": "2026-08-12T14:00:00.000Z",
"refunded_total_cents": 0,
"refunds": [],
"customer": {
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"phone": "+15555550123"
},
"shipping": {
"address": "123 Main St",
"address2": "Apt 4",
"city": "Austin",
"state": "TX",
"zip": "78701",
"country": "US"
},
"items": [
{ "sku": "magnesium-glycinate", "name": "Magnesium Glycinate", "quantity": 2, "unit_price_cents": 2900 }
]
}
}
  • shipping is the ship-to address: the one you supplied at order creation when you sent one, otherwise the address the customer entered on the hosted checkout. null when neither exists yet (typically while status is still payment_pending).
  • customer merges the identity fields the same way โ€” your supplied values win over the customer-entered ones; absent fields are null.

status values:

StatusMeaning
payment_pendingLink minted; the customer has not paid yet.
paidPayment captured โ€” the order is confirmed and moves to fulfilment. paid_at is set. A partially refunded order stays paid; check refunded_total_cents.
expiredThe payment link passed its 7-day expiry without payment. Mint a new order to retry.
cancelledThe transaction was cancelled by Stealth staff.
refundedThe order was refunded in full via POST /partner/store/orders/:transaction_id/refund. refunds[] lists each refund (amount_cents, currency, operation, reason, at).

Errors:

StatusCodeMeaning
400STORE_ORDER_INVALIDMissing transaction_id.
403STORE_ACCESS_REQUIREDProduction access without the store feature flag.
404STORE_ORDER_NOT_FOUNDNo store order with this transaction_id belongs to your partner account.

items reflects what the customer actually paid for: if they removed an optional line on the hosted checkout before paying, the removed line is absent here (and from the webhook payload).


6.5d POST /partner/store/orders/:transaction_id/refundโ€‹

Refund a paid customer-mode store order, partially or in full โ€” built for partners that embed refund handling in their own internal support portal. The transaction_id is the one returned by POST /partner/store/orders. Only your own orders resolve; another partner's transaction_id returns 404.

The money is returned to the card the customer paid with, through the same processor that captured the charge (Authorize.net or Stripe โ€” resolved automatically from the order; you never specify it). Refunds are recorded on the order and surfaced by GET /partner/store/orders/:transaction_id (refunded_total_cents, refunds[], and status: "refunded" once fully refunded). Every refund also emits a store_order.refunded webhook.

Request:

{
"amount_cents": 10000,
"reason": "Customer returned one item"
}
  • amount_cents (optional) โ€” a positive integer in the order's currency (the same denomination as subtotal_cents). Omit it to refund the entire remaining balance. Multiple partial refunds are allowed until the paid amount is exhausted; a request exceeding the remaining balance is rejected with the remaining amount in error.details.remaining_refundable_cents.
  • reason (optional, โ‰ค500 chars) โ€” stored on the refund record and echoed in refunds[] on the status endpoint.

CAD orders. The customer saw (and you request) CAD cents, but Authorize.net charges settle in USD. The refund is converted back at the exact exchange rate stamped on the original charge, and a refund that exhausts the balance releases exactly the remaining charged USD โ€” FX rounding can never over- or under-refund.

Response (200 OK):

{
"refund": {
"transaction_id": "aBc123...",
"status": "partially_refunded",
"operation": "refund",
"processor": "authorize_net",
"refund_ref": "80123456790",
"amount_cents": 10000,
"currency": "cad",
"refunded_total_cents": 10000,
"remaining_refundable_cents": 19500,
"partner_reference": "po_2026_0042",
"created_at": "2026-08-10T18:00:00.000Z"
}
}

status is refunded (nothing left to refund) or partially_refunded. operation is refund for a settled charge, or void when the charge had not yet settled and the full amount was cancelled instead (card networks don't support refunding an unsettled charge).

Settlement timing (Authorize.net orders). A card charge settles overnight (~24 hours). Until then only a full refund is possible (it is executed as a void of the whole charge). A partial refund requested before settlement is rejected with 409 STORE_REFUND_NOT_SETTLED and no money moves โ€” retry it after settlement, or request a full refund instead.

Errors:

StatusCodeMeaning
400STORE_ORDER_INVALIDMissing transaction_id.
400STORE_REFUND_AMOUNT_INVALIDamount_cents is not a positive integer, or exceeds the remaining refundable balance (error.details.remaining_refundable_cents).
403STORE_ACCESS_REQUIREDProduction access without the store feature flag.
404STORE_ORDER_NOT_FOUNDNo store order with this transaction_id belongs to your partner account.
409STORE_ORDER_NOT_REFUNDABLEThe order is unpaid, expired, cancelled, or already fully refunded โ€” or has no refundable charge recorded.
409STORE_REFUND_NOT_SETTLEDPartial refund requested before the charge settled (~24 h). Retry later or refund in full.
502STORE_REFUND_FAILEDThe payment gateway did not approve the refund (message included). Nothing was recorded; safe to retry.

Refunding an order does not cancel fulfilment automatically. If the order has already been dispatched to the warehouse, contact partners@stealth.health to intercept the shipment.


6.5e GET /partner/store/orders/:id/shippingโ€‹

Poll the fulfillment / tracking state of a store order โ€” built for a partner-side cron that syncs shipment status back into your own system. :id accepts either identifier returned by POST /partner/store/orders:

  • the order_id from a white_label_account create response, or
  • the transaction_id from a customer-mode create response (also usable before the customer has paid โ€” the response then reports the payment status with fulfillment: null).

Only your own orders resolve: the order must belong to your white label or have been minted by your partner account. Anything else returns 404.

Response (200 OK):

{
"order": {
"order_id": "order_1755442800_ab12cd",
"transaction_id": null,
"status": "confirmed",
"partner_reference": "po_2026_0042",
"ship_to": {
"address": "123 Main St",
"address2": null,
"city": "Austin",
"state": "TX",
"zip": "78701",
"country": "US"
},
"fulfillment": {
"status": "in_transit",
"status_label": "In Transit",
"carrier": "ups",
"tracking_number": "1Z999AA10123456784",
"tracking_url": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
"estimated_delivery": "2026-08-21",
"shipped_at": "2026-08-18T16:04:11.000Z",
"delivered_at": null,
"updated_at": "2026-08-19T02:15:40.000Z"
},
"created_at": "2026-08-17T14:00:00.000Z"
}
}
  • fulfillment is null until the warehouse ships the order (tracking is ingested from the fulfillment integrations โ€” ShipStation / Easyship โ€” and carrier delivery updates). Poll until fulfillment.status reaches delivered. A shipment can briefly report a status with a null tracking_number (marked shipped before the label's number was assigned).
  • ship_to is the structured address the order will ship (or shipped) to; null when no address is on the order yet.
  • When :id is a transaction_id whose customer hasn't paid, status is the payment status (payment_pending / expired / cancelled) and both order_id and fulfillment are null. After payment, status reflects the order (confirmed) and fulfillment appears once shipped.

Recommended cron cadence: every 15โ€“60 minutes per open order; stop polling once delivered (or after your own retention window).

Errors:

StatusCodeMeaning
400STORE_ORDER_INVALIDMissing id.
403STORE_ACCESS_REQUIREDProduction access without the store feature flag.
404STORE_ORDER_NOT_FOUNDNo store order with this id belongs to your partner account.

Generate a tokenized, time-limited tracking link that the partner can display to their end user. The link opens the Stealth Health patient portal's shipment tracking view without exposing the tracking number to the partner.

Response (200 OK):

{
"referral_id": "ref_abc123xyz",
"tracking_url": "https://portal.stealth.health/track/tkn_9f8e7d6c5b4a...",
"expires_at": "2026-03-05T14:00:00Z"
}

Returns 404 if no shipment exists for the referral yet.


6.7 POST /partner/referrals/:referral_id/cancelโ€‹

Request cancellation of a referral. Only possible if the referral has not yet reached approved status.

Request:

{
"reason": "Customer requested cancellation"
}

Response (200 OK):

{
"referral_id": "ref_abc123xyz",
"status": "cancelled",
"cancelled_at": "2026-03-02T16:00:00Z"
}

Returns 409 Conflict if the referral is already approved or further in the lifecycle.


6.8 GET /partner/eventsโ€‹

Retrieve webhook events that Stealth Health has emitted for the authenticated partner. Use this endpoint to replay events the receiver missed during an outage and to reconcile state after the dead-letter queue has fired.

Every event Stealth Health sends to a partner is also persisted in our internal event store with full delivery metadata (attempt count, last HTTP status, last error). This endpoint is the partner-facing read view over that store โ€” scoped to the calling partner's events only.

Use it when:

  • Your webhook receiver had a sustained outage and you need to backfill events that exhausted the 6-attempt retry budget.
  • You want to verify a specific webhook was actually sent (e.g. a customer support inquiry: "we never got a referral.shipped for ref X").
  • You need to reconcile downstream state after a deploy that introduced a handler bug.

Query Parameters:

ParamTypeDefaultDescription
event_typestring(all)Filter to one event type, e.g. referral.approved, referral.shipped. Repeat the param to OR multiple types.
referral_idstringโ€”Only events for this referral.
partner_referencestringโ€”Only events whose underlying referral matches this partner_reference.
delivery_statusstring(all)delivered (received 2xx), pending_retry (in the retry window), failed (exhausted retries โ€” i.e. dead-letter), suppressed (no webhook URL configured).
occurred_afterISO 860124 h agoLower bound on created_at. Default keeps the response small.
occurred_beforeISO 8601nowUpper bound on created_at.
limitinteger50Max results per page (1โ€“200).
cursorstringโ€”Pagination cursor from a previous response.

Retention. Events remain queryable for 30 days from created_at. Events older than 30 days are purged from the event store; the originating audit-log entries (which are PHI-redacted) are retained for the standard 6-year HIPAA window. If you need durable, long-term storage of every event your integration has ever received, persist them on receipt โ€” do not rely on this endpoint as a system of record.

Response (200 OK):

{
"events": [
{
"event_id": "evt_1a2b3c4d",
"event_type": "referral.shipped",
"referral_id": "ref_abc123xyz",
"partner_reference": "cust_12345",
"data": {
"carrier": "USPS",
"estimated_delivery": "2026-03-07",
"occurred_at": "2026-03-04T11:00:00Z"
},
"metadata": {
"campaign": "spring-2026"
},
"delivery": {
"status": "failed",
"attempt_count": 6,
"first_attempted_at": "2026-03-04T11:00:01Z",
"last_attempted_at": "2026-03-04T23:00:01Z",
"last_http_status": 502,
"last_error": "Bad Gateway"
},
"created_at": "2026-03-04T11:00:00Z"
}
],
"pagination": {
"has_more": false,
"next_cursor": null
}
}

The top-level event shape is identical to what was originally POST-ed to your webhook URL (see ยง 8.2) with one addition: a delivery sub-object describing how that delivery attempt actually went.

Delivery sub-objectโ€‹

FieldTypeDescription
statusstringdelivered, pending_retry, failed, or suppressed.
attempt_countintegerNumber of HTTPS POST attempts made (0 if suppressed).
first_attempted_atISO 8601 | nullTimestamp of the first delivery attempt. null if suppressed.
last_attempted_atISO 8601 | nullTimestamp of the most recent delivery attempt.
last_http_statusinteger | nullHTTP status returned by the partner endpoint on the last attempt.
last_errorstring | nullTruncated error message captured on the last attempt (timeout, connection refused, TLS failure, etc.).

Replaying an eventโ€‹

This endpoint is read-only โ€” it does not re-trigger delivery. To request a manual retry of a failed or suppressed event, contact partners@stealth.health with the event_id. We will re-queue it into the standard retry pipeline. Replay is rate-limited to 100 events per request and 1,000 events per partner per day.

Errorsโ€‹

CodeDescription
INVALID_QUERY_PARAMOne of event_type, delivery_status, or a timestamp param failed validation.
EVENTS_NOT_FOUNDNo events match the query. (Returned with HTTP 200 and an empty events: []; the code appears in pagination.note for callers that prefer to branch on it.)

7. Webhook Eventsโ€‹

7.1 Overviewโ€‹

Stealth Health sends HTTPS POST requests to the partner's registered webhook URL whenever a referral's status changes โ€” and when a hosted store-order checkout is paid (store_order.payment_complete). Events are delivered at least once โ€” partners must handle idempotency.

7.2 Payload Formatโ€‹

{
"event_id": "evt_1a2b3c4d",
"event_type": "referral.approved",
"referral_id": "ref_abc123xyz",
"partner_reference": "cust_12345",
"data": {
"status": "approved",
"product_category": "trt-cream",
"occurred_at": "2026-03-03T09:30:00Z"
},
"metadata": {
"campaign": "spring-2026"
},
"created_at": "2026-03-03T09:30:01Z"
}

7.3 Event Typesโ€‹

EventTriggerdata includes
referral.enrolledPatient completes intake formproduct_category
referral.pending_reviewIntake submitted for doctor reviewโ€”
referral.approvedDoctor approves prescriptionโ€”
referral.deniedDoctor denies prescriptiondenial_category (e.g. medical_contraindication, incomplete_information, not_a_candidate)
referral.payment_duePayment required from patientamount_cents, currency
referral.payment_completePatient payment receivedamount_cents, currency
referral.awaiting_shipmentPrescription sent to pharmacyโ€”
referral.shippedOrder shipped by pharmacycarrier, estimated_delivery
referral.in_transitShipment in transit (carrier scan)carrier
referral.out_for_deliveryShipment out for deliverycarrier
referral.deliveredShipment deliveredcarrier, delivered_at
referral.delivery_exceptionDelivery issue (failed attempt, etc.)carrier, exception_type
referral.cancelledReferral cancelledcancelled_by (partner or patient)
referral.expiredEnrollment URL expired without completionโ€”
store_order.payment_completeCustomer paid a hosted store-order checkout link (ยง 6.5b, customer mode)transaction_id, order_id, payment_mode, currency, customer (first_name, last_name, email, phone), shipping (address, address2, city, state, zip, country), items[] (sku, name, quantity, unit_price_cents)
store_order.refundedA store order was refunded via POST /partner/store/orders/:id/refundtransaction_id, refund_status (refunded / partially_refunded), amount_cents, currency, operation (refund / void), remaining_refundable_cents

Store-order events carry referral_id: null (there is no referral โ€” the order came from POST /partner/store/orders) and echo the partner_reference you supplied at order creation. Correlate on data.transaction_id or partner_reference. Like every event, delivery is at-least-once: dedupe on event_id (or treat a repeat transaction_id as idempotent).

data.shipping is the ship-to address for the order: the address you supplied at order creation when you sent one, otherwise the address the customer entered on the hosted checkout. It is null only when neither exists (e.g. the checkout completed without an address on file โ€” contact support to resolve). data.customer merges the identity fields the same way (your supplied values win over the customer-entered ones).

7.4 Signature Verificationโ€‹

import hmac
import hashlib

def verify_signature(payload_body: bytes, signature_header: str, webhook_secret: str) -> bool:
expected = "sha256=" + hmac.new(
webhook_secret.encode(),
payload_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)

7.5 Retry Policyโ€‹

AttemptDelay
1Immediate
230 seconds
35 minutes
430 minutes
52 hours
612 hours

After 6 failed attempts (non-2xx response or timeout), the event is placed in a dead letter queue. Partners can retrieve missed events via GET /partner/events (see ยง 6.8).

Partners must respond with a 2xx status code within 10 seconds to acknowledge receipt.


8. Data Modelsโ€‹

8.1 Referral Objectโ€‹

{
"referral_id": "string โ€” Stealth Health's unique identifier",
"partner_reference": "string โ€” Partner's customer identifier",
"product_category": "string โ€” e.g. trt-cream, peptides",
"status": "string โ€” See Appendix: Status Lifecycle",
"status_history": [
{ "status": "string", "at": "ISO 8601 timestamp" }
],
"fulfillment": {
"status": "string | null",
"carrier": "string | null",
"estimated_delivery": "string (YYYY-MM-DD) | null",
"updated_at": "ISO 8601 timestamp | null"
},
"payment": {
"status": "string โ€” due | paid | refunded | not_applicable",
"amount_cents": "integer | null",
"currency": "string โ€” USD | CAD",
"paid_at": "ISO 8601 timestamp | null"
},
"metadata": "object โ€” Partner-provided key-value pairs",
"created_at": "ISO 8601 timestamp",
"expires_at": "ISO 8601 timestamp โ€” Enrollment link expiry"
}

8.2 Event Objectโ€‹

{
"event_id": "string โ€” Unique event identifier (for idempotency)",
"event_type": "string โ€” e.g. referral.approved",
"referral_id": "string",
"partner_reference": "string",
"data": "object โ€” Event-specific payload (see Section 7.3)",
"metadata": "object โ€” Mirror of referral metadata",
"created_at": "ISO 8601 timestamp"
}

9. Error Handlingโ€‹

9.1 HTTP Status Codesโ€‹

CodeMeaning
200Success
201Created
400Bad request โ€” invalid parameters
401Unauthorized โ€” invalid or missing API key
403Forbidden โ€” valid key but insufficient permissions
404Not found โ€” referral doesn't exist or doesn't belong to this partner
409Conflict โ€” action not allowed in current state
422Unprocessable โ€” valid syntax but business rule violation
429Rate limited
500Internal server error

9.2 Error Response Formatโ€‹

{
"error": {
"code": "REFERRAL_NOT_CANCELLABLE",
"message": "Referral ref_abc123xyz cannot be cancelled because it has already been approved.",
"details": {
"referral_id": "ref_abc123xyz",
"current_status": "approved"
}
}
}

9.3 Common Error Codesโ€‹

CodeDescription
INVALID_PARTNER_REFERENCEpartner_reference already used or invalid format
INVALID_PRODUCT_CATEGORYCategory not available to this partner
REFERRAL_NOT_FOUNDReferral doesn't exist or unauthorized
REFERRAL_NOT_CANCELLABLEReferral past the cancellable stage
REFERRAL_EXPIREDEnrollment link has expired
STORE_ACCESS_REQUIREDProduction store access requires the store feature flag (addendum)
STORE_ORDER_INVALIDMissing/invalid items or customer.email on a store order
STORE_PAYMENT_MODE_INVALIDpayment.mode must be customer or white_label_account
STORE_PRICING_INVALIDpayment.pricing must be retail or clinic_use; clinic_use requires white_label_account mode
STORE_PRODUCT_UNAVAILABLERequested SKU(s) not in this white label's store
WHITE_LABEL_BILLING_NOT_CONFIGUREDNo card on file for white_label_account charge
WHITE_LABEL_CARD_DECLINEDThe white label's card on file was declined
STORE_CHARGE_FAILEDUpstream billing/charge error
RATE_LIMITEDToo many requests

10. Rate Limitsโ€‹

TierRequests / minuteBurst
Sandbox6010/sec
Production30050/sec
EnterpriseCustomCustom

Rate limit headers are included in every response:

X-RateLimit-Limit: 300
X-RateLimit-Remaining: 298
X-RateLimit-Reset: 1709395200

11. Environmentsโ€‹

EnvironmentBase URLPurpose
Sandboxhttps://sandbox.stealth.healthTesting with mock data, no real doctors or pharmacy
Productionhttps://api.stealth.healthLive environment

Sandbox behavior:

  • Referrals auto-advance through the lifecycle every 60 seconds.
  • Payment always succeeds with test card 4242 4242 4242 4242.
  • Webhooks are sent to the registered sandbox webhook URL.
  • No real clinical review occurs.

Disabling the lifecycle simulator (for testing real review / sign / reject). The auto-advancer races every referral to delivered, which prevents you from exercising a real prescriber review. Two opt-outs are available:

  • Tenant-wide โ€” set disable_auto_advance: true on your partner_configs record (ask your Stealth contact, or set it in the reporting portal). The simulator skips your tenant entirely; referrals stay where you (or a real doctor) put them.
  • Per-referral โ€” set manual: true on an individual partner_referrals doc to freeze just that case while the rest of your sandbox keeps simulating.

With the simulator off, a case submitted via POST /partner/appointments lands in the Stealth doctor review queue and only advances when a Stealth doctor signs (emits appointment.prescription_signed for clinical-tier partners) or rejects (emits referral.denied).


Appendix: Status Lifecycleโ€‹

Terminal states: delivered, denied, cancelled, expired


Questions for Partner Discussionโ€‹

  1. Revenue share model โ€” How will partner compensation be structured? Per-referral fee, revenue share, or flat monthly?
  2. Branding depth โ€” Does the partner want full white-label (custom domain, emails from their domain) or co-branded?
  3. Product categories โ€” Which categories does the partner want to offer?
  4. Jurisdiction โ€” US only, Canada only, or both?
  5. Patient support โ€” Will the partner handle first-line patient support, or will Stealth Health?
  6. Reporting needs โ€” Does the partner need additional reporting endpoints beyond the summary endpoint?
  7. Refund handling โ€” How should refunds be coordinated between partner and Stealth Health?
  8. Re-enrollment โ€” Should the same customer be able to enroll for additional product categories, and how should that be linked?

This document is a proposal for discussion purposes. Endpoint paths, field names, and behaviors are subject to change during implementation.