Self-processed checkout integration
Self-processed checkout integration
Keep checkout on your own payment rail and let Truemed handle the HSA/FSA side: eligibility, the shopper’s health survey, the Letter of Medical Necessity (LMN), and substantiation. You charge the card and report the charge to Truemed, and Truemed produces the compliance artifacts and the reconciliation data.
This is the model behind a Shop Pay self-processed checkout. The shopper pays through your existing flow, and a Truemed session runs alongside it to qualify the purchase and record it for HSA/FSA purposes. Truemed never moves money here. Every capture and refund you report describes a charge you settled yourself.
How the flow works
A cart that needs an LMN takes the longest path: survey first, then the hold, then review.
Shorter paths exist. A fully pre-approved cart, or a shopper whose existing LMN already covers the cart, is ready for the hold as soon as you create the session, and never leaves your checkout. See Pre-approved carts and Returning customers and LMN reuse.
One session tracks one checkout from cart to settlement:
- Create a session with the cart. Truemed returns the eligibility breakdown and, when the shopper needs the survey, a hosted-survey URL.
- Send the shopper to the survey to establish their LMN. Truemed returns them to your
return_url. - Read the session before you take the card. The status tells you what to do next; in the
common case it’s
pending_authorization, so you place the hold and signal it, which starts clinical review. - Capture when review finishes. Read the per-item amounts to size the capture, capture the hold on your rail, and report it.
- Report refunds as the order evolves. Cancel instead if the session never captured.
Reading qualification at the return
The shopper arriving back on return_url is navigation, not a decision. Two things tell you where
qualification actually stands, and they answer different questions.
GET /api/v2/shop_pay/truemed_sessions/{id} is the authoritative read. Call it when the shopper
returns and gate on qualification_info.qualification_status.
Webhooks cover the transitions that happen while the shopper isn’t in front of you, review finishing
above all. Delivery rides a background worker, so a webhook can arrive before or after the shopper
returns. A missing webhook doesn’t mean “not ready” at the moment the shopper lands. A session emits
truemed_session.pending_authorization at most once, when intake completes, and later exactly one
terminal truemed_session.truemed_review_complete, whose qualification_status says whether review
approved or rejected the request.
Pre-approved carts and reused LMNs never send the shopper anywhere, so the table below applies only to sessions that went through the survey. Any of these statuses can be live when the shopper lands, depending on how far qualification has progressed:
Before you start
Every request carries a platform API key (x-truemed-api-key) and the Shopify shop GID
(x-truemed-shop-id), which selects the store your key acts on. The examples below use the
production base URL https://api.truemed.com.
Amounts are always integer cents. The truemed_session_id from the create response identifies the
session in every later call.
Walk through a checkout
Create the session
POST /api/v2/shop_pay/truemed_sessions/create with the cart, the shopper’s details, an
idempotency_key scoped to your shop, and the return_url for Truemed to send the shopper back
to after the survey.
Report the order’s shipping charge in the optional cart_shipping block, alongside
order_items. Include tax_cents when you charge shipping tax separately:
cart_shipping covers all shipping on the order, so leave shipping out of the line items’
amount_details. Truemed adds it to the authorized total and shows it in the receipt’s Shipping
row. It sits outside the eligibility split, so a fully pre-approved cart with shipping still
skips intake.
Truemed always creates the session, even for a cart with nothing eligible, and the response is
where you read the outcome. cart_info splits the cart into eligible, ineligible, and
pre-approved amounts, and reports cart shipping in shipping_amount_cents and
shipping_tax_cents. Both shipping fields are 0 when the cart has no shipping.
Send the shopper to the survey
When redirect_url is present, redirect the shopper there to complete the health survey and
establish their LMN. Truemed returns them to the return_url you supplied.
A null redirect_url means there’s nowhere to send them, and
qualification_info.qualification_status says why:
rejected, witheligible_amount_centsandpreapproved_amount_centsboth0: every item is HSA/FSA-ineligible, so there’s no qualification path. Collect payment on your own rail as a normal order.pending_authorization: no intake is needed, because the cart is fully pre-approved or because the shopper’s existing LMN covers it. Neither case emits atruemed_session.pending_authorizationwebhook, since there’s no intake to complete. The create response is your signal to place the hold.
Read the session before you take the card
Fetch the session with GET /api/v2/shop_pay/truemed_sessions/{truemed_session_id} and read
qualification_info.qualification_status. That status decides what happens next; the
return-gate table covers every value. approved means
review already resolved, so capture. rejected means there’s no HSA/FSA path, so collect as an
ordinary order.
In the common case the status reads pending_authorization, and the session waits there for you:
signaling the authorization hold (or reporting a capture) starts clinical review. Size the hold
from cart_info.eligible_amount_cents, the create-time estimate. The qualified amounts land on
the session’s line items at terminal review and can differ from that estimate. When both the
eligible and pre-approved amounts are 0, there’s no HSA/FSA path to pursue.
Place the hold on the shopper’s card on your own rail, then signal it with
POST /api/v2/shop_pay/truemed_sessions/{truemed_session_id}/auth_holds/create. The call takes no
request body. Signal it only once intake is resolved: a hold signaled while the session still
reads pending_user_intake returns a 400. The response echoes qualification_status, which
reads pending_truemed_review on an LMN cart, since signaling the hold starts review, or
approved on a cart that needs no review.
A webhook-driven integration can drive the hold off truemed_session.pending_authorization,
which fires the moment intake completes, and use the read above for reconciliation.
Capture when review finishes
Clinical review resolves asynchronously and ends with exactly one
truemed_session.truemed_review_complete event. Subscribe to it, and to
truemed_session.pending_authorization if you drive the hold off the webhook, so you never need
to poll.
The payload points at the session rather than carrying a full snapshot:
Fetch GET /api/v2/shop_pay/truemed_sessions/{truemed_session_id} and read
qualification_status, then size the capture from each line’s eligible_amount_cents and
preapproved_amount_cents. Review decides how much of each item qualifies, so the per-item split
is what sizes the capture, not the create-time cart_info.eligible_amount_cents estimate.
- On
approved, capture the hold for the sum of the qualified line amounts (eligible_amount_cents+preapproved_amount_cents) and report it. - On
rejected, review has zeroed the LMN-dependent eligible portion. Capture anypreapproved_amount_cents, since pre-approved items never needed the LMN, release the rest, and collect the balance as an ordinary non-HSA/FSA sale.
Every order item on the session detail carries three amount buckets: eligible_amount_cents,
preapproved_amount_cents, and ineligible_amount_cents. They’re mutually exclusive and sum to
the line total once eligibility is known. eligible_amount_cents covers the LMN-dependent
portion alone, so a pre-approved line reports its amount under preapproved_amount_cents and
0 eligible.
The buckets evolve with the session. Before the shopper completes the survey, the
eligible/ineligible split is unknown and both fields read null, leaving only the pre-approved
portion visible. After the survey they reflect the catalog split, and after review they’re final.
A rejection moves the LMN-dependent eligible amount into ineligible and leaves any pre-approved
amount alone. The values survive cancellation, so eligible_amount_cents > 0 on a canceled
session’s items means the session was approved before it was canceled.
If you miss a delivery, the same read returns the full qualification state, so use it to reconcile.
Report the capture
Once you’ve captured the hold, report the charge with
POST /api/v2/shop_pay/truemed_sessions/{truemed_session_id}/capture. Send a capture_info block
describing the settled charge and an idempotency_key scoped to your shop.
Truemed derives the captured amount from the session’s order items, so there’s no amount field in
the request; item_details identifies which lines you captured. When a line carried
amount_details at creation, its capture entry repeats that accounting: a value for every
original component, where 0 claims none of it, plus any new entries. Renaming, re-leveling, or
dropping an original component is rejected. That decomposition keeps the HSA/FSA receipt’s
line, order, tax, and shipping presentation correct after partial captures.
The response confirms what Truemed recorded: the session’s cumulative captured amount and the capture id.
Report refunds and cancellations
Issue refunds and cancellations on your own rail first, then report them so the session history stays accurate. Capture is the dividing line: cancel before it, refund after it. Once a session has captured money, giving it back is a refund.
Refund: POST /api/v2/shop_pay/truemed_sessions/{truemed_session_id}/refunds/create with an
idempotency_key and refund_amount_cents, the authoritative refunded amount. Add
refund_line_items (item_id and quantity) to classify how much of the refund was
HSA/FSA-eligible, or omit them for an amount-only refund. A refund cannot exceed the session’s
captured amount net of prior refunds.
Cancel: POST /api/v2/shop_pay/truemed_sessions/{truemed_session_id}/cancel when the shopper
never finished or you withdrew the offer, any time before a capture is reported. Cancel takes no
body and is idempotent, so re-canceling a canceled session is a safe no-op. Canceling a session
that already captured returns a 400.
Authorization holds
You place the authorization hold on the card ahead of clinical review, then signal it to Truemed
with POST /api/v2/shop_pay/truemed_sessions/{truemed_session_id}/auth_holds/create. The call is a
status notification and takes no request body.
Truemed accepts the hold once intake is resolved, so wait for pending_authorization before
signaling; a hold signaled while the session still reads pending_user_intake returns a 400. On a
cart that needs an LMN, signaling the hold starts clinical review and the session advances to
pending_truemed_review. A fully pre-approved cart has nothing to review, so it goes straight to
approved. You capture the hold after approval and release it if the shopper is rejected.
Returning customers and LMN reuse
An LMN stays valid for about a year, so a shopper who qualified in March can skip the survey in
June. When Truemed already holds a verified LMN that covers the cart, the session reuses it at
creation: the create response comes back with qualification_status = pending_authorization and
redirect_url = null, and your authorization hold is the only remaining gate.
Send a stable user_id on every session you create, your durable identifier for the shopper. A
Shopify customer id works well. It ties a shopper’s sessions together for reconciliation, and it’s
what the returning-customer match keys off as that lands.
Matching currently keys off the shopper’s checkout email, so a shopper who checks out under a new
email is treated as new. Keying it off the stable user_id you send is a fast-follow. Branch on
the create response as described below and your integration picks up the wider match with no
changes.
qualification_status and redirect_url together cover the three ways a create can land:
A reused LMN needs no clinical review, and there’s no intake to complete, so this path emits neither
webhook. After signaling the hold, read the session with
GET /api/v2/shop_pay/truemed_sessions/{truemed_session_id}: it reads approved once the hold is
accepted and the reused LMN is attached to it. The hold response itself can read
pending_truemed_review, because that attachment lands just after the call returns. Capture off the
read.
Pre-approved carts
Some HSA/FSA-eligible products are pre-approved: they qualify without an LMN, so there’s no survey
to take and no clinical review to wait for. When every eligible item in a cart is pre-approved, the
session skips intake. The create response comes back with qualification_status = pending_authorization and redirect_url = null, and your authorization hold is the only remaining
gate.
truemed_session.pending_authorization fires when survey intake completes, so a fully pre-approved
cart never emits it. Place the hold off the create response, which already carries
qualification_status = pending_authorization.
A cart with any LMN-dependent item walks the full ladder instead: pending_user_intake → survey →
pending_authorization → hold → pending_truemed_review → terminal. A pure pre-approved cart never
enters pending_truemed_review, so the hold signal moves it straight to approved.
Known-ineligible items in a mixed cart sit in the ineligible bucket on the per-item split and leave
the status alone.
Both paths use the same branching you already have: redirect_url at create, approved before
capture. A pre-approved line’s amount sits in preapproved_amount_cents with
eligible_amount_cents at 0, and capture sizing stays the sum of the two.
Reconciliation and listing sessions
GET /api/v2/shop_pay/truemed_sessions lists your channel’s sessions, newest first, for
reconciliation and dashboards. Filter with user_id, qualification_status, search, and the
created_* and captured_* date ranges. Page with page and page_size (default 30, max 100).
Each row is a summary: session id, cart and qualification info, and timestamps. That’s enough to
drive most reconciliation on its own. For line items, captures, refunds, and auth holds, fetch the
session by id with GET /api/v2/shop_pay/truemed_sessions/{truemed_session_id}.
Session statuses
qualification_status summarizes the HSA/FSA eligibility decision:
You own the payment state on your own rail. The capture, refund, and cancel responses confirm each action you report with recorded amounts and ids, so there’s no separate Truemed-side money status to reconcile against.
Idempotency and safe retries
Create, capture, and refund each take an idempotency_key scoped to your shop. Replaying a key with
identical contents returns the originally recorded result instead of double-recording it, so retries
after a network blip are safe. Reusing a key with a different payload returns a 400. Cancel and
the auth-hold signal carry no body and are idempotent on their own, so repeating them is a safe
no-op.
Full request and response schemas for every endpoint live in the API Reference.