The entry link
The hosted checkout lives at/checkout on your member portal address — the same address where
your patients sign in and manage their plan, named in your
welcome kit:
Entry context selects what to present — never what anything costs. Malformed values are
dropped at the door, and every money figure on the page is resolved by the server from your
configured catalog and pricing. A link cannot name a price, a discount, or a fee; a request that
tries is rejected.
Two ways in
Your entry mode is set for your account when your program is set up, and a single offering can be set differently: questions-first (clinical_before_pay) or checkout-first
(clinical_after_pay). Your welcome pack shows each offering’s mode, which setting decided it,
and the exact checkout link to build. A questions-first link carries offering_ref and
journey_id only. A checkout-first link carries offering_ref, sku_id and therapy.
- Questions-first. Your storefront’s server starts the health questions with
GET /v1/instrument/resolve?offering=<offering_ref>; the first resolve mints the patient’s enrollment and returns itsjourney_id. The answer that completes the questions returns a one-time checkout pass,destination_capability, to your server only; it lasts about 10 minutes. Your storefront hands the patient over with a top-level form POST tohttps://{MEMBER_HOST}/api/checkout/door, with the pass in thecapabilityfield and youroffering_refbeside it, from a page on one of your registered browser origins. The door checks the pass and sends the patient to checkout with theirjourney_idattached: identity, eligibility context, and enrollment are already in place, and payment is the only step left. Every refusal is the same plain not-found. If the pass expires, the patient starts the questions again; it is never reissued from the enrollment id. See handing the patient to checkout for the form, the origin rule and what the patient sees. - Checkout-first. Where your program is set up to take payment before the health questions,
a buy link carries
sku_idandtherapyas well (?offering_ref=SEMA-1M&sku_id=sku_b7945bba07e4&therapy=weight_loss). All three must be one offering’s values, exactly as your welcome pack and onboarding contact give them. The enrollment is started at the door: the hosted checkout’s own server callsPOST /v1/prospectsto open it. That operation is not yet part of the published API reference — your storefront never calls it; the hosted surface makes the call on its own server. Which way your deployment works is configuration agreed when your program is set up, never inferred from the link — on a questions-first deployment, a link without an enrollment never opens the payment form: the payment step stays locked and tells the patient to start with their health questions.
A reference implementation
The site kit ships a reference implementation of this entry contract. The hand-off composer,checkout-links.mjs in the site kit repository, builds the entry link for both ways in above — carrying
journey_id for the questionnaire-first entry, and taking sku_id and therapy for direct buy.
Read it as the worked example of the link this page specifies: the table above is the source of
truth for the wire, and the composer is a ready way to build it. For a questions-first offering,
the hand-off is the form POST to the checkout door described above, not a link your site opens.
Sign-in at the door
Checkout requires sign-in, and the checkout handles it itself. Your storefront may also send the patient through your member portal’s sign-in link,https://{MEMBER_HOST}/auth/login?returnTo=/home, before checkout; that step is optional. The patient must
have a member portal session before they pay: the order belongs to the account that is logged in
to the portal at payment, and only that account’s portal shows it. Do not use the API’s
/v1/auth/login for this: it does not sign the patient in to the member portal (see
Login URLs and domains).
A patient who is logged in sees their identity collapsed to one bar with a sign-out. A patient
who arrives logged out starts with an email field (“Continue with email”), then signs in or signs
up on the same hosted sign-in page with a password, a passkey, or Google before paying. The
checkout page itself never embeds a password form. Your storefront does
not create the patient’s account. So signing in is required, and the checkout handles it:
payment stays locked until the patient is logged in. Your storefront’s own sign-in step is optional.
Payment credentials never touch your code
The standing rule, stated plainly: no payment credential is ever handled client-side — not in your storefront, and not in the checkout page’s own frontend either. Card collection happens on the hosted page using Stripe’s browser components: the card number travels from the patient’s browser directly to the payment provider and never passes through the platform, or through you. There is no card field for you to build, no token for you to relay, and nothing payment-shaped for you to store. The browser side runs on the payment provider’s publishable key — not one of yours — and today that key is test-scoped only; a live-scoped key is refused outright. Your own publishable brand key is a separate credential with a different purpose, described in authentication. Behind the page, the hosted server creates the session withPOST /v1/payments/checkout-sessions under an idempotency key, so a retried submit continues
the same session instead of creating a second charge — and the required consent documents
configured for your deployment must be agreed to before that call is ever made. If no consent
documents are configured yet, payment stays locked: a patient is never charged without agreeing
to the configured set.
That create also proves that the caller holds the enrollment it names: the hosted checkout sends
the enrollment’s capability on the x-held-capability header. On a checkout-first offering that
is the capability POST /v1/prospects returned to the hosted checkout’s own server. On a
questions-first offering it is the one-time checkout pass your storefront handed over with the
form POST to the door above. A create that names a journey_id without a capability is refused
with 404, the same answer as for an enrollment that does not exist, and nothing is created. So
do not start a session from your server: hand the patient to the hosted checkout, and let it
create the session. See handing the patient to checkout.
How your servers learn the outcome
After paying, the patient continues inside the portal — there is no browser redirect back to your storefront, and the outcome never rides the patient’s browser to reach you. Your servers learn what happened two ways:- Deliveries you subscribe to. Register an endpoint with
POST /v1/webhooks/registrations; the response returns your signing secret once, and every delivery carries a signature computed with it so your receiver can verify the sender. Deliveries are reference-only — an event type plus identifiers such as the order reference — so a delivery prompts a read but never leaks detail on its own. You can prove the pipe end to end at any time withPOST /v1/webhooks/registrations/{registration_id}/test. - Reads you make.
GET /v1/payments/{order_ref}/statusreturns the payment state of the order aspayment_state, fed by confirmed events from the payment provider, never by guesswork. Its values arecreated(the order exists and is awaiting payment),payment_authorized,paid,payment_failed,partially_refunded,refunded,disputedandcancelled. Branch on those exact values.GET /v1/journeys/{journey_id}/statusreturns the patient’s public enrollment status, and a patient never shows as approved before their order is authorized — so the enrollment read is safe to drive your storefront’s own status copy. Read the payment status for the order’s own state: the enrollment read may return no status yet instead of a status value, and it would never carry payment detail in any case.
payment_authorized rather than paid, and
it is still a working order: paid is not required to confirm one. The event journey.payment.succeeded.v1 is not proof of a charge. To confirm a
test order, see
Confirm your first test order.
Two facts worth designing around: a failed payment still leaves a readable order — the attempt
is never invisible — and promo codes are not supported yet; the field is reserved. The hosted
checkout shows a promo code field, and it refuses every code: the page
says “Promo codes aren’t available yet. Remove the code to continue.” and clears the code, and the
patient pays your stored price. Do not offer your patients promo codes.