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:
- Partner directs their customer to a co-branded Stealth Health enrollment page.
- Patient completes a medical intake questionnaire and (optionally) pays.
- Stealth Health doctors review the intake and approve or deny a prescription.
- Stealth Health pharmacy partners fulfill and ship approved prescriptions.
- 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 Point | Example |
|---|---|
| Partner-scoped reference ID | ref_abc123xyz |
| Enrollment status | enrolled, pending_review, approved, denied |
| Fulfillment status | awaiting_shipment, shipped, delivered |
| Timestamp of each status change | 2026-03-02T14:30:00Z |
| Product category enrolled for | trt-cream, peptides |
| Payment status | payment_due, payment_complete |
| Tracking carrier (no tracking #) | USPS, UPS, Canada Post |
What Partners CANNOT Accessโ
| Data Point | Reason |
|---|---|
| Patient name, DOB, gender | PHI โ identity |
| Address, phone, email | PHI โ contact info |
| Medical conditions, symptoms, allergies | PHI โ clinical |
| Prescription details (medication, dosage, quantity) | PHI โ treatment |
| Doctor notes, intake responses | PHI โ clinical |
| Lab results, ID verification images | PHI โ clinical / PII |
| Tracking numbers, shipping labels | Could 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:
| Credential | Purpose |
|---|---|
X-Partner-ID | Identifies the partner (public, safe to log) |
X-Api-Key | Authenticates 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:
| Item | Description |
|---|---|
partner_id | Unique partner identifier (e.g. ptr_acme_health) |
| API key pair | Live and sandbox credentials |
| Webhook secret | For 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 config | Logo, colors, and copy for co-branded enrollment pages |
| Webhook URL | Partner'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โ

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"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
partner_reference | string | Yes | Partner's own identifier for this customer. Must be unique per partner. Max 128 chars. |
product_category | string | Yes | One of the available categories (see 6.5). |
metadata | object | No | Arbitrary 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:
| Param | Type | Default | Description |
|---|---|---|---|
status | string | (all) | Filter by status: created, enrolled, pending_review, approved, denied, expired |
fulfillment_status | string | (all) | Filter by fulfillment: awaiting_shipment, shipped, in_transit, delivered |
partner_reference | string | โ | Look up by partner's own reference |
product_category | string | (all) | Filter by category |
created_after | ISO 8601 | โ | Only referrals created after this timestamp |
created_before | ISO 8601 | โ | Only referrals created before this timestamp |
limit | integer | 50 | Max results per page (1โ200) |
cursor | string | โ | 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:
| Param | Type | Description |
|---|---|---|
period | string | day, week, month, all_time (default: month) |
product_category | string | Filter 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"
}
]
}
skuis 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.mode | Behaviour |
|---|---|
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_account | Charges 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:
- Webhook (push, recommended): when the customer completes payment,
a
store_order.payment_completeevent POSTs to your registered webhook URL, carrying thetransaction_id, yourpartner_reference, the paid line items, and thecustomer+shippingblocks (the ship-to address the customer entered on the hosted checkout, or the one you supplied at order creation). - Polling:
GET /partner/store/orders/:transaction_idreturns the order's current status (payment_pending/paid/expired/cancelled) plus the samecustomer+shippingblocks 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.pricing | Behaviour |
|---|---|
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_use | The 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 everywhite_label_accountorder โ 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[]acceptssku(preferred),product_id, orid;quantityis clamped to 1โ99.customer.emailis required.partner_reference(โค120 chars) is echoed back and used as the idempotency seed forwhite_label_accountcharges โ 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:
| Status | Code | Meaning |
|---|---|---|
| 400 | STORE_ORDER_INVALID | Missing items or customer.email. |
| 400 | STORE_PAYMENT_MODE_INVALID | payment.mode not customer/white_label_account. |
| 400 | STORE_PRICING_INVALID | payment.pricing not retail/clinic_use, or clinic_use requested on a customer-mode order. |
| 400 | STORE_PRODUCT_UNAVAILABLE | None of the requested SKUs are in this white label's store. |
| 402 | WHITE_LABEL_CARD_DECLINED | The card on file was declined. |
| 409 | WHITE_LABEL_BILLING_NOT_CONFIGURED | No card on file โ add one in the reporting portal. |
| 403 | STORE_ACCESS_REQUIRED | Production access without the store feature flag. |
| 502 | STORE_CHARGE_FAILED | Upstream 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 }
]
}
}
shippingis 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.nullwhen neither exists yet (typically whilestatusis stillpayment_pending).customermerges the identity fields the same way โ your supplied values win over the customer-entered ones; absent fields arenull.
status values:
| Status | Meaning |
|---|---|
payment_pending | Link minted; the customer has not paid yet. |
paid | Payment captured โ the order is confirmed and moves to fulfilment. paid_at is set. A partially refunded order stays paid; check refunded_total_cents. |
expired | The payment link passed its 7-day expiry without payment. Mint a new order to retry. |
cancelled | The transaction was cancelled by Stealth staff. |
refunded | The order was refunded in full via POST /partner/store/orders/:transaction_id/refund. refunds[] lists each refund (amount_cents, currency, operation, reason, at). |
Errors:
| Status | Code | Meaning |
|---|---|---|
| 400 | STORE_ORDER_INVALID | Missing transaction_id. |
| 403 | STORE_ACCESS_REQUIRED | Production access without the store feature flag. |
| 404 | STORE_ORDER_NOT_FOUND | No store order with this transaction_id belongs to your partner account. |
itemsreflects 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 assubtotal_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 inerror.details.remaining_refundable_cents.reason(optional, โค500 chars) โ stored on the refund record and echoed inrefunds[]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:
| Status | Code | Meaning |
|---|---|---|
| 400 | STORE_ORDER_INVALID | Missing transaction_id. |
| 400 | STORE_REFUND_AMOUNT_INVALID | amount_cents is not a positive integer, or exceeds the remaining refundable balance (error.details.remaining_refundable_cents). |
| 403 | STORE_ACCESS_REQUIRED | Production access without the store feature flag. |
| 404 | STORE_ORDER_NOT_FOUND | No store order with this transaction_id belongs to your partner account. |
| 409 | STORE_ORDER_NOT_REFUNDABLE | The order is unpaid, expired, cancelled, or already fully refunded โ or has no refundable charge recorded. |
| 409 | STORE_REFUND_NOT_SETTLED | Partial refund requested before the charge settled (~24 h). Retry later or refund in full. |
| 502 | STORE_REFUND_FAILED | The 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_idfrom awhite_label_accountcreate response, or - the
transaction_idfrom acustomer-mode create response (also usable before the customer has paid โ the response then reports the payment status withfulfillment: 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"
}
}
fulfillmentisnulluntil the warehouse ships the order (tracking is ingested from the fulfillment integrations โ ShipStation / Easyship โ and carrier delivery updates). Poll untilfulfillment.statusreachesdelivered. A shipment can briefly report a status with anulltracking_number(marked shipped before the label's number was assigned).ship_tois the structured address the order will ship (or shipped) to;nullwhen no address is on the order yet.- When
:idis atransaction_idwhose customer hasn't paid,statusis the payment status (payment_pending/expired/cancelled) and bothorder_idandfulfillmentarenull. After payment,statusreflects 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:
| Status | Code | Meaning |
|---|---|---|
| 400 | STORE_ORDER_INVALID | Missing id. |
| 403 | STORE_ACCESS_REQUIRED | Production access without the store feature flag. |
| 404 | STORE_ORDER_NOT_FOUND | No store order with this id belongs to your partner account. |
6.6 GET /partner/referrals/:referral_id/tracking-linkโ
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
404if 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 Conflictif 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.shippedfor ref X"). - You need to reconcile downstream state after a deploy that introduced a handler bug.
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
event_type | string | (all) | Filter to one event type, e.g. referral.approved, referral.shipped. Repeat the param to OR multiple types. |
referral_id | string | โ | Only events for this referral. |
partner_reference | string | โ | Only events whose underlying referral matches this partner_reference. |
delivery_status | string | (all) | delivered (received 2xx), pending_retry (in the retry window), failed (exhausted retries โ i.e. dead-letter), suppressed (no webhook URL configured). |
occurred_after | ISO 8601 | 24 h ago | Lower bound on created_at. Default keeps the response small. |
occurred_before | ISO 8601 | now | Upper bound on created_at. |
limit | integer | 50 | Max results per page (1โ200). |
cursor | string | โ | 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โ
| Field | Type | Description |
|---|---|---|
status | string | delivered, pending_retry, failed, or suppressed. |
attempt_count | integer | Number of HTTPS POST attempts made (0 if suppressed). |
first_attempted_at | ISO 8601 | null | Timestamp of the first delivery attempt. null if suppressed. |
last_attempted_at | ISO 8601 | null | Timestamp of the most recent delivery attempt. |
last_http_status | integer | null | HTTP status returned by the partner endpoint on the last attempt. |
last_error | string | null | Truncated 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โ
| Code | Description |
|---|---|
INVALID_QUERY_PARAM | One of event_type, delivery_status, or a timestamp param failed validation. |
EVENTS_NOT_FOUND | No 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โ
| Event | Trigger | data includes |
|---|---|---|
referral.enrolled | Patient completes intake form | product_category |
referral.pending_review | Intake submitted for doctor review | โ |
referral.approved | Doctor approves prescription | โ |
referral.denied | Doctor denies prescription | denial_category (e.g. medical_contraindication, incomplete_information, not_a_candidate) |
referral.payment_due | Payment required from patient | amount_cents, currency |
referral.payment_complete | Patient payment received | amount_cents, currency |
referral.awaiting_shipment | Prescription sent to pharmacy | โ |
referral.shipped | Order shipped by pharmacy | carrier, estimated_delivery |
referral.in_transit | Shipment in transit (carrier scan) | carrier |
referral.out_for_delivery | Shipment out for delivery | carrier |
referral.delivered | Shipment delivered | carrier, delivered_at |
referral.delivery_exception | Delivery issue (failed attempt, etc.) | carrier, exception_type |
referral.cancelled | Referral cancelled | cancelled_by (partner or patient) |
referral.expired | Enrollment URL expired without completion | โ |
store_order.payment_complete | Customer 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.refunded | A store order was refunded via POST /partner/store/orders/:id/refund | transaction_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 fromPOST /partner/store/orders) and echo thepartner_referenceyou supplied at order creation. Correlate ondata.transaction_idorpartner_reference. Like every event, delivery is at-least-once: dedupe onevent_id(or treat a repeattransaction_idas idempotent).
data.shippingis 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 isnullonly when neither exists (e.g. the checkout completed without an address on file โ contact support to resolve).data.customermerges 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โ
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 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โ
| Code | Meaning |
|---|---|
200 | Success |
201 | Created |
400 | Bad request โ invalid parameters |
401 | Unauthorized โ invalid or missing API key |
403 | Forbidden โ valid key but insufficient permissions |
404 | Not found โ referral doesn't exist or doesn't belong to this partner |
409 | Conflict โ action not allowed in current state |
422 | Unprocessable โ valid syntax but business rule violation |
429 | Rate limited |
500 | Internal 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โ
| Code | Description |
|---|---|
INVALID_PARTNER_REFERENCE | partner_reference already used or invalid format |
INVALID_PRODUCT_CATEGORY | Category not available to this partner |
REFERRAL_NOT_FOUND | Referral doesn't exist or unauthorized |
REFERRAL_NOT_CANCELLABLE | Referral past the cancellable stage |
REFERRAL_EXPIRED | Enrollment link has expired |
STORE_ACCESS_REQUIRED | Production store access requires the store feature flag (addendum) |
STORE_ORDER_INVALID | Missing/invalid items or customer.email on a store order |
STORE_PAYMENT_MODE_INVALID | payment.mode must be customer or white_label_account |
STORE_PRICING_INVALID | payment.pricing must be retail or clinic_use; clinic_use requires white_label_account mode |
STORE_PRODUCT_UNAVAILABLE | Requested SKU(s) not in this white label's store |
WHITE_LABEL_BILLING_NOT_CONFIGURED | No card on file for white_label_account charge |
WHITE_LABEL_CARD_DECLINED | The white label's card on file was declined |
STORE_CHARGE_FAILED | Upstream billing/charge error |
RATE_LIMITED | Too many requests |
10. Rate Limitsโ
| Tier | Requests / minute | Burst |
|---|---|---|
| Sandbox | 60 | 10/sec |
| Production | 300 | 50/sec |
| Enterprise | Custom | Custom |
Rate limit headers are included in every response:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 298
X-RateLimit-Reset: 1709395200
11. Environmentsโ
| Environment | Base URL | Purpose |
|---|---|---|
| Sandbox | https://sandbox.stealth.health | Testing with mock data, no real doctors or pharmacy |
| Production | https://api.stealth.health | Live 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: trueon yourpartner_configsrecord (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: trueon an individualpartner_referralsdoc 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โ
- Revenue share model โ How will partner compensation be structured? Per-referral fee, revenue share, or flat monthly?
- Branding depth โ Does the partner want full white-label (custom domain, emails from their domain) or co-branded?
- Product categories โ Which categories does the partner want to offer?
- Jurisdiction โ US only, Canada only, or both?
- Patient support โ Will the partner handle first-line patient support, or will Stealth Health?
- Reporting needs โ Does the partner need additional reporting endpoints beyond the summary endpoint?
- Refund handling โ How should refunds be coordinated between partner and Stealth Health?
- 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.