Skip to main content
The platform hosts the page where a patient pays. Your storefront never renders a card field, never holds a payment credential, and never relays a payment token — it hands the patient to the hosted checkout with a link, and your servers learn the outcome afterwards. This page describes that door: the entry URL, what the link carries, and the reads that tell you what happened.
Building in Lovable? Skip this tip. The site kit below is not the Lovable path: follow Connect a Lovable storefront instead.Fastest start for everyone else — the site kit. You do not have to build this door from scratch. The platform provides a fork-and-own storefront starter: a complete Next.js site that already stands up the storefront leading to this door, including the hand-off link that brings a patient here. Fork it, brand it, and deploy it as your own.Fork a tagged release — never the tip of the main branch — so your site is pinned to a known, supported version. The starter lives in the site kit repository, which is a public repository: fork the latest tagged release from its releases page.The in-repo site-kit-quickstart guide is your operator’s manual — it takes a fork from first run to a live, branded site.What the starter gives you is the site itself: the storefront and the brand slots, plus a working reference for the calls this page describes. What the platform issues you is what makes that site yours — your credentials at onboarding, your brand, and your assigned offerings. The starter ships identical to everyone; your configuration is what makes it your brand’s door.The site kit is not the Lovable path. If you are building your storefront in Lovable, do not use the site kit: follow Connect a Lovable storefront instead.

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:
The link carries the entry context as query parameters: 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 its journey_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 to https://{MEMBER_HOST}/api/checkout/door, with the pass in the capability field and your offering_ref beside it, from a page on one of your registered browser origins. The door checks the pass and sends the patient to checkout with their journey_id attached: 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_id and therapy as 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 calls POST /v1/prospects to 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 with POST /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 with POST /v1/webhooks/registrations/{registration_id}/test.
  • Reads you make. GET /v1/payments/{order_ref}/status returns the payment state of the order as payment_state, fed by confirmed events from the payment provider, never by guesswork. Its values are created (the order exists and is awaiting payment), payment_authorized, paid, payment_failed, partially_refunded, refunded, disputed and cancelled. Branch on those exact values. GET /v1/journeys/{journey_id}/status returns 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.
An order that is held on the platform’s side may read 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.