API calls

API calls, at a glance

CallWhenWhat it does
Create Payment TokenSignup, onceStarts the survey and dual-card journey. Returns the URL you embed and a provision-request id.
Payment Token Updated (webhook)LMN decidedTells you the token exists and carries the clinical outcome in eligibility_status. It arrives on approval and on rejection (then with zero HSA/FSA coverage). Your signal that the first order can be charged.
Retrieve Provision Token RequestOptionalPolling fallback if you would rather not depend solely on the webhook.
Retrieve Eligibility by Payment TokenMenu renderReturns eligibility_status, items (the meals this customer may put on their HSA/FSA card) and lmn_expires_at.
Retrieve Eligibility by Provision RequestFirst order, before the token existsThe same response, keyed on the provision-request id from signup.
Retrieve Catalog EligibilityMenu with no customerYour whole configured catalog, with no customer-specific fields.
Create Payment SessionEvery chargeThe whole transaction: classifies each line, splits across both cards, authorizes, echoes the expiry date.
Capture SessionOptionalIf you would rather authorize at selection and capture at fulfilment, this is how.
Payment Session Complete (webhook)Every chargeAsynchronous outcome of the charge.
Create RefundAs neededRefunds return to the originating card, split the same way as the charge.

Every call in this table is live in production today and documented in the API reference.

The four calls that carry a customer through signup

Four payloads, matching steps 1, 3, 5 and 6a of the first-order diagram. Host tokens resolve to the environment you are reading this on; sandbox and production hosts are listed in Before you begin.

Step 1: Start the journey

Create payment token:

1POST https://api.truemed.com/api/v1/payment_tokens/create
2x-truemed-api-key: <your API key>
3Content-Type: application/json
4
5{
6 "idempotency_key": "merchant-signup-84213",
7 "customer_email": "jordan.reyes@example.com",
8 "customer_name": "Jordan Reyes",
9 "customer_state": "CA",
10 "success_url": "https://www.example.com/hsa/complete",
11 "failure_url": "https://www.example.com/hsa/cancelled",
12 "metadata": "{\"merchant_customer_id\":\"84213\"}",
13 "use_iframe": true
14}

That is the whole of the customer information Truemed needs, and each field does one job:

  • customer_email: required. Truemed uses it to create the patient record the LMN is written for, and it is where the receipt goes. It is not how you look the customer up afterwards; that is the token’s job, and the distinction matters, because an email address can change while the person does not.
  • customer_name: required. It goes on the LMN, and it pre-fills the name confirmation step in the survey so the customer confirms rather than retypes. It is read once, when the record is first created, so a later name change on your side does not travel through this call.
  • customer_state: optional, and worth sending. It pre-fills the state-of-residence answer, which determines which practitioner can review the case. It is the only survey answer this call can pre-fill.
  • metadata: an opaque string Truemed stores and hands back to you on the token webhook and on every payment session. This is where your own customer ID goes. There is no dedicated external-ID field on this endpoint today; if you would rather have a first-class one, say so and we will scope it.
  • order_items: deliberately absent. Your channel qualifies against the whole catalog, so there is no basket to declare. (For other partners it is required.)
  • use_iframe: true, which is what makes the returned URL embeddable. See Embedding the flow.

Step 2: Read what comes back

1{
2 "provision_token_request_id": "b1f0c7a2-5d3e-4a19-9c6b-2f8e41d07a55",
3 "redirect_url": "https://app.truemed.com/survey/6c2d9e41-7b83-4f52-a0d1-3ea95c6b8f27"
4}

Use redirect_url as the src of your iframe. That is step 2 of the first-order flow, the health survey and the two cards, rendered inside a modal on your page.

Persist provision_token_request_id on the customer record before you open the frame. It is the join key. The decision comes back asynchronously, minutes or hours later, on a webhook that carries no email and no name, so this id (or your own id inside metadata) is how you know which customer it belongs to.

Step 3: Receive the token

payment_token.updated:

1POST https://<your webhook endpoint>
2x-truemed-signature: t=1785938402,v0=8f4c2ad91e6b...
3Content-Type: application/json
1{
2 "webhook_delivery_id": "dlv_9a71c0e4b5d2483f8e6c1a70d94b2f53",
3 "event_type": "payment_token.updated",
4 "data": {
5 "payment_token": "tm_token_4e2a91c8-70bd-4f36-9a15-c8b3d6e07f42",
6 "provision_token_request_id": "b1f0c7a2-5d3e-4a19-9c6b-2f8e41d07a55",
7 "payment_session_id": null,
8 "failed_payment_session_id": null,
9 "eligibility_status": "approved",
10 "payment_methods": [
11 { "last4": "4242", "expiration": "08/2029",
12 "payment_method_updated_at": "2026-08-06T14:22:07.318000+00:00" },
13 { "last4": "1881", "expiration": "03/2028",
14 "payment_method_updated_at": "2026-08-06T14:22:07.402000+00:00" }
15 ],
16 "lmn_expires_at": "2027-08-06T14:19:55+00:00",
17 "metadata": "{\"merchant_customer_id\":\"84213\"}"
18 }
19}

Acknowledge with a 2xx; Truemed retries with backoff for up to seven days. The payload is a full snapshot of current state rather than a delta, so the safe way to consume it is to upsert, overwriting the token and card details you hold. That stays correct under a retry and under a genuine later update alike.

  • payment_token: the value you charge against. Its shape is tm_token_<uuid4>, 45 characters; treat it as an opaque string rather than parsing it. It is the only field on this payload guaranteed to be non-null.
  • eligibility_status: the clinical outcome, one of approved, pending, expired, rejected or never_requested. On a first order it is approved or rejected. The other three can reach you on a later delivery for the same token, so branch on the value rather than treating it as a boolean. It is an additive optional field, so a handler written before it existed keeps working unchanged.
  • payment_methods: last four and expiry for both stored cards, HSA/FSA first, so you can render “HSA •••• 4242 · regular •••• 1881” in account settings without a second call.
  • lmn_expires_at: the 12-month LMN expiry described in Subsequent orders. It is the same date you get back on every payment session.
  • The envelope shown is what a signed endpoint receives, with an HMAC-SHA256 over the timestamp and body in x-truemed-signature. We recommend it. An endpoint registered without a signing secret instead receives the inner data object at the top level, authenticated with x-truemed-api-key.

The rejected delivery is the same event, and three fields tell it apart. It is worth having side by side with the payload above:

1{
2 "webhook_delivery_id": "dlv_3c7e0b52d1a94f60b8ae1d7f2c05e934",
3 "event_type": "payment_token.updated",
4 "data": {
5 "payment_token": "tm_token_3c7e0b52-d1a9-4f60-b8ae-1d7f2c05e934",
6 "provision_token_request_id": "f81a4e60-90c7-4d2b-8ab3-71e5c9042fd7",
7 "payment_session_id": null,
8 "failed_payment_session_id": null,
9 "eligibility_status": "rejected",
10 "payment_methods": [
11 { "last4": "1881", "expiration": "03/2028",
12 "payment_method_updated_at": "2026-08-06T14:22:07.402000+00:00" }
13 ],
14 "lmn_expires_at": null,
15 "metadata": "{\"merchant_customer_id\":\"84219\"}"
16 }
17}

The token is still minted, so the outcome can be correlated back to the provision_token_request_id you persisted. It simply covers nothing, and every line of every charge routes to the regular card.

Where the token lives on your side

One token per customer, stored as a field on the customer or subscription record, in the same place you would keep a payment-method id. It is what every later create payment session call is keyed on, including the unattended weekly ones, so it needs to be readable from your recurring-billing job as well as from a checkout session.

Two properties are worth designing around. The token is long-lived and writable: if the customer later replaces a card, the token value stays the same and Truemed sends payment_token.updated again, with the same payment_token, the same provision_token_request_id and refreshed payment_methods. So handle a repeat delivery as an update to the record you already have. The token is also durable: it survives LMN expiry. An expired LMN leaves the token valid and means the HSA/FSA portion stops being eligible, so everything routes to the regular card until a new LMN is in place.

Step 4: Read eligibility, before and after the decision

Retrieve Eligibility by Provision Request:

1GET https://api.truemed.com/api/v1/eligibility/provision_request/b1f0c7a2-5d3e-4a19-9c6b-2f8e41d07a55
2x-truemed-api-key: <your API key>
1{
2 "eligibility_status": "pending",
3 "items": ["salmon-teriyaki-bowl", "chicken-pesto-plate", "protein-breakfast-box"],
4 "lmn_expires_at": null
5}

This is the read behind step 3 of the first-order flow. The LMN is still with the practitioner, so items is what the survey predicts it will cover and there is no expiry yet. Badge these as provisional. The charge still waits for step 5’s webhook.

This is the same one call whether or not a person picks the meals. If the customer chooses their first box by hand, you use the response to badge the menu they are looking at. If your personalization engine assembles the box for them with no manual selection, you use the identical response to decide which lines are HSA/FSA-payable before you charge. The eligibility call answers the same question in both cases.

Once the token is minted, the same read keyed on the token answers with the decided coverage. This is the call every later order opens with:

1GET https://api.truemed.com/api/v1/eligibility/token/tm_token_4e2a91c8-70bd-4f36-9a15-c8b3d6e07f42
2x-truemed-api-key: <your API key>
1{
2 "eligibility_status": "approved",
3 "items": ["salmon-teriyaki-bowl", "chicken-pesto-plate", "protein-breakfast-box"],
4 "lmn_expires_at": "2027-08-06T14:19:55+00:00"
5}

If the practitioner declines, the same read answers rejected, the same value the webhook already carried, available here whenever you want to confirm it:

1GET https://api.truemed.com/api/v1/eligibility/provision_request/b1f0c7a2-5d3e-4a19-9c6b-2f8e41d07a55
2x-truemed-api-key: <your API key>
1{
2 "eligibility_status": "rejected",
3 "items": [],
4 "lmn_expires_at": null
5}

The token still arrives on payment_token.updated with zero HSA/FSA coverage, so the held order charges fully to the regular card and ships. Use this outcome for messaging.

All three forms are live and share one response shape. Called bare, as GET /api/v1/eligibility with no key, it returns your whole configured catalog with eligibility_status and lmn_expires_at both null: the menu-with-no-customer read. The state table below spells out items and lmn_expires_at for every value.

Five states, five different things to do

Every form of GET /api/v1/eligibility, whether bare, keyed on /token/{token_id}, or keyed on /provision_request/{provision_token_request_id}, answers with the same three-field response shape: eligibility_status, items, lmn_expires_at. The field tells you where the customer stands, and you decide what your lifecycle messaging does about it. First, what each value carries:

eligibility_statusitemslmn_expires_at
pending (normally the provision-request read)What the survey predicts the LMN will cover, provisional until the practitioner decides. Empty only while the survey is unfinished.null, because there is no LMN yet. On a token read, pending means a requalification is in flight, and it carries the previous LMN’s expiry.
approvedThe items the customer may put on the HSA/FSA card.In the future: the date coverage ends.
expiredEmpty.In the past: the date coverage ended.
rejectedEmpty.null on a first order, where no LMN was ever issued; the old LMN’s expiry when a lapsed subscriber was re-declined.
never_requestedEmpty, unless your catalog needs no LMN at all, in which case the pre-approved items, since nothing depends on an LMN.null, because no LMN was ever requested for this customer on this channel.

The bare catalog read, GET /api/v1/eligibility with no key, returns every item configured for you with eligibility_status and lmn_expires_at both null: there is no shopper to have either.

The states are not interchangeable, and one of them is a reason to hold back:

StateUseful to you?Right behavior
PendingYes. It is what lets you badge the first menu before the LMN is decided.Badge the predicted items and label them provisional. The first charge still waits for the webhook.
ApprovedYes. The steady state, and the only one where the customer can pay with the HSA/FSA card.Badge the listed items as HSA/FSA-payable and charge. Refresh the read when your menu changes.
ExpiredYes, high value. This is the renewal trigger, and the one to build lifecycle messaging on.Nudge to requalify, and keep charging the regular card meanwhile.
Never requestedYes. It distinguishes “hasn’t tried” from “tried and was declined”, which calls for an acquisition prompt rather than a re-engagement one.Offer the qualification link. The copy differs from the renewal nudge.
RejectedYes, mainly as a suppression signal.Do not prompt. Charge the regular card silently.

Never requested is usually yours to know without asking, though not always. The read is keyed on ids only you hold: the token, or the provision-request id for the first order. If you are holding neither for a customer, they have never started the journey, and there is nothing to call. What that reasoning misses is that a customer can hold a token while no LMN was ever requested for them, so holding an id is not proof that a decision was ever made. When you call the read in that situation, Truemed answers never_requested rather than folding the case into expired, which would assert a an LMN that never existed and point you at a renewal nudge when what the customer needs is a first-time prompt.

Rejected is the one worth handling carefully. A customer whose LMN lapsed, who requalified and was then declined, still has a live token and a working regular card, so everything keeps shipping and keeps charging. What should not happen is another renewal nudge: a practitioner has already reviewed and declined, and Truemed has already emailed them about it. Reading eligibility_status, rather than inferring from an empty items list, is what keeps you from asking twice. A first-order rejection lands in the same place from day zero: the token arrives covering nothing, and the subscription starts on the regular card instead of dying at the gate.