Self-Processed Checkout Integration
Self-Processed Checkout Integration
The Truemed Session API lets you keep checkout on your own payment rail while Truemed handles the HSA/FSA side: eligibility, the shopper’s health survey, the Letter of Medical Necessity (LMN), and substantiation. You charge the card; you report the charge to Truemed; Truemed produces the compliance artifacts and 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.
How the flow works
A single session tracks one checkout from cart to settlement:
- Create a session with the cart. Truemed returns the cart’s eligibility breakdown and a hosted-survey URL.
- Send the shopper to the survey so they can establish their LMN, then bring them back to your
return_url. - Gate the return, then place the hold. Landing back on
return_urldoesn’t by itself say where qualification stands — readqualification_info.qualification_statusbefore taking the card.approvedwithis_already_qualifiedmeans a returning qualified shopper: skip review and go straight to hold + capture. Otherwise place the authorization hold and signal it — the hold response’squalification_statustells you which world you’re in:pending_truemed_reviewmeans intake is done and clinical review is running;pending_user_intakemeans the shopper never finished the survey, so collect payment as an ordinary, non-HSA/FSA order. - Capture on the webhook. The terminal
truemed_session.updateddelivery is the money gate: its payload carries the authoritativequalification_status.approvedorpartially_approved— fetch the session, capture the hold for the qualified amount (the sum of the line items’eligible_amount_cents+preapproved_amount_cents), and report it;rejected— void the hold and collect any remainder as an ordinary order. - Report refunds — or cancel a session that never captured — as the order evolves. Once you’ve captured, the session can no longer be canceled; report refunds instead.
Truemed never moves money in this flow — you do. Every capture and refund you report describes a charge you already settled yourself.
Before you start
You’ll need a Truemed API key, passed in the x-truemed-api-key header on every request. All
examples below use the production base URL https://api.truemed.com.
Amounts are always integer cents. The truemed_session_id Truemed returns at creation
identifies the session in every later call.
Walk through a checkout
Create the session
POST /api/v2/truemed_sessions/create with the cart, the shopper’s details, an
idempotency_key scoped to your channel, and the return_url Truemed should send the shopper
back to after the survey.
A session is always minted — even for a fully ineligible cart — so the response is a uniform
place to read eligibility, never a rejection. cart_info decomposes the cart into eligible,
ineligible, and pre-approved amounts.
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 once they’re done.
redirect_url is null when there’s nowhere to send the shopper. Don’t hardcode the reason —
read qualification_info.qualification_status to know which case you’re in:
rejectedwithcart_info.cart_type=all_ineligible— every item is HSA/FSA-ineligible, so there’s no qualification path; collect payment on your own rail as a normal, non-HSA/FSA order.pending_authorizationat create — a fully pre-approved cart (see Pre-approved carts): no LMN is needed, so there’s no survey to take — place the hold and signal it.approved— reserved for LMN reuse (see Returning customers): the shopper already holds a covering LMN, so there’s no survey to take — go straight to hold and capture. Branching on the status now means you pick up the skip-the-survey experience the day it ships, with no integration change.
Gate the return, then place the hold
When the shopper lands back on your return_url, don’t take the card yet — the return alone
doesn’t say where qualification stands. Fetch the session with
GET /api/v2/truemed_sessions/{truemed_session_id} and read
qualification_info.qualification_status:
approved(orpartially_approved) — the session is already approved (for example, the shopper qualified on an earlier visit to this session, or — once LMN reuse ships — Truemed matched an existing LMN at creation). No review is needed: place the hold, signal it, and capture.pending_authorization— intake is done (the shopper finished the survey, or the cart is fully pre-approved and never needed one), and the session is parked here until you act: nothing further happens until you signal your authorization hold (or report a capture), which is what starts clinical review. Before placing the hold, readcart_info—cart_typetells you whether there’s an HSA/FSA path at all, andeligible_amount_cents(the create-time estimate) sizes the hold. The qualified amounts land on the session’s line items at terminal review and can differ from the estimate.pending_user_intake— the shopper never finished the survey (uncommon at return, but real: an abandoned survey followed by manual navigation back). How to proceed is your call: re-offer the survey (the session’sredirect_urlkeeps working while it’s pending), or collect payment as an ordinary, non-HSA/FSA order. If they finish the survey later, the session picks up frompending_authorization.)
With intake complete, place the authorization hold on the shopper’s card on your own
rail, then signal it with
POST /api/v2/truemed_sessions/{truemed_session_id}/auth_holds/create (no request body). The
response’s qualification_status advances to pending_truemed_review — clinical review is
now running; wait for the webhook. (Holds can also be signaled before the survey finishes:
the session holds at pending_user_intake and review starts automatically at survey
completion.)
Prefer webhooks over the GET here too: the review-begins truemed_session.updated delivery
(qualification_status = pending_truemed_review) fires the moment review starts, so a
webhook-driven integration only ever needs the GET for the is_already_qualified check and
for reconciliation.
Capture on the webhook — the money gate
Clinical review resolves asynchronously. Subscribe to the truemed_session.updated webhook: it
fires when review begins, when it reaches a terminal result, and on each payment change you
report — so you never poll. The terminal delivery carries the decision; the session detail
carries the qualified amounts:
data.qualification_info.qualification_statusmust beapprovedorpartially_approvedbefore you treat the purchase as HSA/FSA-substantiated (partially_approvedmeans the shopper qualified but part of the cart is ineligible — the per-item split tells you which lines). Onrejected, void the hold and collect on a regular card as an ordinary order.- On approval, fetch
GET /api/v2/truemed_sessions/{truemed_session_id}and read each line’seligible_amount_centsandpreapproved_amount_cents— review decides how much of each item actually qualifies, so the per-item split (not the create-timecart_info.eligible_amount_centsestimate) sizes the capture. Capture the hold for the sum of the qualified line amounts (eligible_amount_cents+preapproved_amount_cents).
Every order item on the session detail carries three amount buckets: eligible_amount_cents,
preapproved_amount_cents, and ineligible_amount_cents. The buckets are mutually exclusive
and sum to the line total once eligibility is known — eligible_amount_cents covers only the
LMN-dependent portion, so a pre-approved line reports its amount under
preapproved_amount_cents with eligible 0. They evolve with the session: before the shopper
completes the survey the eligible/ineligible split is unknown (the fields are omitted) and only
the pre-approved portion shows; once the survey is in they reflect the catalog split; after
review they’re final — a rejection zeroes the LMN-dependent eligible portion into ineligible,
while any pre-approved amount is unaffected (it never needed the LMN). 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 ever miss a delivery, the same GET returns the full qualification state — use it to reconcile, not as a required step before every charge.
Report the capture
Once you’ve captured the hold, report the charge with
POST /api/v2/truemed_sessions/{truemed_session_id}/capture. Send a transaction_info block
describing the settled charge and a merchant_transaction_id for the charge on your side.
The captured amount is derived from the session’s order items, not trusted from the request,
so there’s no separate amount field — item_details identifies which lines you captured. When a
line carried amount_details at creation, its capture entry must carry the same accounting:
a value for every original component (0 = none of it), optionally plus new entries — never a
renamed, re-leveled, or omitted one. The same rule applies to refund item_details, and it’s
what keeps the HSA/FSA receipt’s line/order/tax/shipping presentation correct after partial
captures and refunds.
The response confirms the recorded capture — the cumulative captured amount and Truemed’s capture id.
Report refunds and cancellations
Issue refunds and cancellations on your own rail first, then report them to Truemed so the session history stays accurate. The dividing line is capture: cancels happen before capture, refunds happen after — once money has been captured on a session, giving it back is always a refund.
- Refund:
POST /api/v2/truemed_sessions/{truemed_session_id}/refunds/createwith anidempotency_keyand the itemizeditem_details— the refunded amount is derived from the line totals, the same way capture works, so there’s no separate amount field. Truemed records the refund against the session’s history. - Cancel:
POST /api/v2/truemed_sessions/{truemed_session_id}/cancelwhen the shopper never finished or you withdrew the offer — before anything was captured. A captured session can no longer be canceled (the call returns a400); money coming back after capture is a refund. Cancel takes no body and is idempotent — re-canceling an already-canceled session is a safe no-op.
Authorization holds
You place an authorization hold on the card ahead of clinical review, then signal it to Truemed with
POST /api/v2/truemed_sessions/{truemed_session_id}/auth_holds/create. This is a status
notification only (no request body): it advances the session to pending_truemed_review and
triggers LMN review once the shopper has completed the survey. After approval you capture that hold
(reported as your capture); if the shopper is rejected, you void it.
Returning customers and LMN reuse
Send a stable user_id — your durable identifier for the shopper (a Shopify customer id works
well) — on every session you create. It’s what ties a shopper’s sessions together, and it’s the
key that unlocks the returning-customer experience: an LMN is valid for about a year, so a
shopper who qualified in March doesn’t have to re-take the survey in June.
When Truemed can match the user_id to an existing LMN that covers the cart, the create response
comes back already decided — no survey, no review, no waiting:
The contract carries everything this needs — is_already_qualified and a nullable redirect_url —
so an integration that branches on the create response’s qualification_status (redirect when
there’s a redirect_url, capture-path when it’s already approved) handles
returning customers automatically.
Pre-approved carts
Some HSA/FSA-eligible products are pre-approved: they qualify without a Letter of Medical
Necessity, so there is 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 entirely — the create response comes back
with qualification_status = pending_authorization and redirect_url = null, and the only
remaining gate is your authorization hold:
Contrast with the standard flow: a cart with any LMN-dependent item walks the full ladder
(pending_user_intake → survey → pending_authorization → hold → pending_truemed_review →
terminal). A pure pre-approved cart never enters pending_truemed_review — there is nothing to
review — so the hold signal moves it straight to approved (or partially_approved when the cart
also carries ineligible items).
An integration that follows this guide handles both flows with no extra branching: you already
gate on redirect_url presence at create and on approved/partially_approved before capturing.
On session reads, a pre-approved line’s amount sits in preapproved_amount_cents (with
eligible_amount_cents = 0 — the buckets are mutually exclusive), and capture sizing is the same
sum of eligible_amount_cents + preapproved_amount_cents.
Reconciliation and listing sessions
GET /api/v2/truemed_sessions lists your channel’s sessions, newest first, for reconciliation and
dashboards. Filter with user_id, qualification_status, search, and the
created_* / captured_* date ranges. Page with page and page_size (default 30, max 100).
Each row returns a lean summary — session id, cart and qualification info, and timestamps —
enough to drive most reconciliation directly. For a session’s full detail (line items, captures,
refunds, auth holds), fetch it by id with
GET /api/v2/truemed_sessions/{truemed_session_id}.
Session statuses
qualification_status summarizes the HSA/FSA eligibility decision:
You already own the payment state on your own rail — the capture, refund, and cancel responses confirm each action you report (recorded amounts and ids), so you never have to reconcile a separate Truemed-side money status.
Idempotency and safe retries
Create and refund requests take an idempotency_key; captures are idempotent on the pair of your
channel and merchant_transaction_id. Replaying the same key (or merchant transaction) returns the
originally recorded result rather than double-recording, so retries after a network blip are safe.
Reusing a key with a different payload returns a 400 validation error.
Full request and response schemas for every endpoint live in the API Reference.