brd_acme and the offering SEMA-1M. Substitute your own brand id and offerings.
The site kit is not the Lovable path. The platform’s site kit is a separate Next.js starter
that you fork and deploy yourself. If you are building in Lovable, ignore it: this page and the
prompt below are your whole path. Do not mix the two.
What your storefront owns, and what the platform provides
Your storefront never renders a card field, never holds a payment credential, and never creates a
patient account. It presents a price, and hands the patient to a platform-hosted page with a link.
The full contract for that hand-off is the checkout door —
read it alongside this page; nothing here replaces it.
The four values, and where each may live
The publishable key is brand-scoped and read-only. It is issued by your onboarding contact, per
brand, and shown once — the same handling as your API key, minus the secrecy. It reaches exactly
two operations and is refused everywhere else, writes included:
GET /v1/account/brands/{brand_id}/storefront— one offering’s price and your brand’s readiness to sell it, in one call.GET /v1/account/brands/{brand_id}/readiness— the readiness report on its own. Its brand-wide checks include your payment account and your checkout consent documents: payment stays locked until the consent documents are configured.
Permitted browser origins
A browser will not let your page read a cross-origin API response unless the platform grants it. The grant is a list of exact origins on your brand’s member domain row, and you manage it yourself. The same list decides where a questions-first hand-off may come from: the checkout door accepts a form POST only from an origin on it (see handing the patient to checkout).- Exact origins only — a wildcard entry is refused. Scheme, host and port, nothing else. You
cannot register a pattern that covers every preview URL Lovable hands you; register the specific
origins you intend to use. The response returns the entries it refused, by name, so a typo tells
you which one rather than presenting as “the browser read is broken”. Plain
httpis accepted only for a loopback host (localhost,127.0.0.1,[::1]), so a local preview can usehttp://localhost:5173; every other origin must behttps. - Each Lovable address is its own origin. The editor preview and the published app are served from different hosts, and each one you load the page from needs its own entry. Copy the origin from the address bar of the page that makes the call: the scheme and host, with no path.
- It is a replace. Removing an origin from the array revokes it.
- Your brand needs a member domain registered first. Without one there is no row to hold the list. See login URLs and domains.
Two switches, both yours to ask for. The browser read needs a publishable key issued for your
brand and your page’s origin on the list above. Until both are in place every cross-origin call
is refused, whatever credential it carries. Build against the server pattern below either way, and
switch the read to the browser once you have both.Both operations declare either credential in the reference — your server key or your
publishable key — so the interactive playground can exercise them with either one. The origin
allowlist is what the playground cannot reproduce: test the cross-origin path from your own page.
The architecture to build against
Lovable projects commonly pair a generated frontend with a server function (its Supabase integration, or any backend you already run). Use that server side. It is not optional styling — it is where your API key has to live.Development setup
The names below are the ones in your welcome kit.
The example offering on this page is
offering_ref SEMA-1M, sku_id sku_b7945bba07e4,
therapy weight_loss. Use your own.
Everything on this site targets the test-mode environment with test-scoped keys and synthetic data
only — see environments.
An offering also needs product copy, a price, billing terms and your checkout consent documents
before a patient can pay for it. The welcome kit lists those steps and the call that adopts the
starter product copy: see
before a price is payable.
Which way in: your entry mode
Your entry mode is set for your account when your program is set up, and a single offering can be set differently. Your link cannot change it.
Your welcome pack shows each offering’s entry mode, which setting decided it (set for this
offering, the account’s setting, or the platform default when none is set), and the exact checkout
link to build. Build the path the pack shows for each offering, not a path you assume. On a
questions-first offering your storefront does not open the pack’s link itself: the checkout door
sends the patient there after the form POST described in Path A. A checkout link that carries no
journey_id never opens the payment form. The payment step stays locked, and the page tells the
patient: “This deployment completes
your health questions before payment, and this link carries no enrollment — start with your health
questions and you’ll come back here with everything attached.” A patient who is not logged in sees
the sign-in lock first. Your storefront never calls POST /v1/prospects: on a checkout-first offering
the platform’s own hosted checkout makes that call on its server.
Signing the patient in
Your storefront may send the patient through your member portal’s sign-in link before checkout:/v1/auth/login: it
returns to the API host and does not sign the patient in to the member portal (see
login URLs and domains).
Path A — questions first
Every call on this path is a server call. Your page can render questions and collect answers, but it can never talk to these endpoints itself. Plan the backend before you plan the pages. The questionnaire is served one question at a time, by the server, which owns sequence, branching and completion. See how questions work for the model.node_id. It keys the answer to the node it was collected for. With it, a stale
replay of an already-passed node resyncs cleanly — 200, current node re-served — and a submit for
a node the session has not reached fails closed with a renderer.node_mismatch issue (422).
Without it, an answer can misbind to whatever node follows a server-side skip, on a path that
carries a patient’s health answers.
The codes answer above is for a choice control. Text, email, number, date and phone controls send
{"value": ...} instead, and a number pair, an address and a file capture each take their own
shape — every control and its answer is listed in
controls and their answers. Repeat step 2
while status is active, and stop when it is complete, blocked or abandoned.
Handing the patient to checkout
A checkout link carryingjourney_id cannot complete checkout on this path. Naming an enrollment
does not prove it is this patient’s, so the checkout refuses it. What proves it is a one-time checkout pass
that the platform gives your server when the questions are complete.
1. Read destination_capability from the completing response. The POST /v1/instrument/next
response whose answer completes the questionnaire (status is complete) carries a
destination_capability string. It is there only when all of these hold:
- that call is the one that completed the questionnaire. A later call of yours on a session that is already complete returns no pass (if the platform has to redeliver the completion internally, the pass it returns is minted fresh for the same enrollment, never a stored copy served again);
- your server started the session itself, with your secret API key, as a fresh start: no
X-Journey-Idheader, so the first resolve created the enrollment; - the questionnaire was served in one pass, with no
phase. Phase-split questionnaires are not yet available on this path (see splitting the questionnaire).
capability field, with your offering_ref beside
it:
journey_id: the door reads the enrollment from the pass itself. Do
not replace the form with fetch or a link. The door accepts only a POST whose body is a
form (application/x-www-form-urlencoded or multipart/form-data), and a GET is refused.
3. Post from a registered origin. The door accepts the POST only when the browser’s Origin
header exactly equals one of your brand’s registered browser origins, which is the same list
described under permitted browser origins. This applies on Path A
even if you never use a publishable key. The Origin the browser sends is the origin of the page
that holds the form. Register every address a patient can finish the questions from: your
published storefront and each named preview or editor host you test from. If the form page is served by a server function on a different host from your
storefront, register that host’s origin. A missing, null or unlisted origin is refused.
4. The 10-minute window. The pass lasts about 10 minutes from the answer that completed
the questions. The door checks it, and so does the checkout when the patient submits their
details, so the hand-off, any sign-in and the checkout submission all have to happen inside that
window. Hand off as soon as the questions complete. An expired pass is never reissued from the
enrollment id. If the window lapses, the patient starts the questionnaire again from your
storefront: a fresh resolve with no X-Journey-Id, which creates a new enrollment, and whose
completion carries a new pass.
What the patient sees:
- Accepted. The door sends the patient straight to your hosted checkout, wearing your brand, for the offering you posted. A patient who is not logged in signs in there first. Payment then works as on any other offering.
- Refused at the door. An expired, missing or malformed pass, a pass for another brand, a
GET, or an unlisted origin all get the same plain not-found response from your member portal address. It is not a branded page and it does not say which check failed. Nothing is created and nothing is charged. - Window lapsed after the door. If the patient reaches checkout but submits after the pass has expired, the checkout shows “We couldn’t confirm this enrollment — nothing was charged.” and nothing is charged. On this path the way back is your storefront’s questionnaire, started again.
- The platform could not check the pass. The door answers that the service is unavailable, sets nothing and charges nothing.
Splitting the questionnaire around the payment
GET /v1/instrument/resolve takes an optional phase parameter that serves one tier of the
questionnaire on its own:
400. Whether your brand can serve a phase therefore depends on its questionnaire configuration —
confirm that with your onboarding contact before you design a page around the split.
Not yet available: a phase-split questionnaire cannot reach checkout yet. It will once the
platform issues the checkout pass at the qualification completion, the last step before checkout.
That change is being built, and this page and the changelog will say when it
ships. Until then a phase=qualification completion carries no destination_capability, so for a
questions-first checkout, serve the whole questionnaire in one pass.
Four constraints to design around:
- Server-side only. These operations need the API key, they carry patient answers, and they are not on the browser-safe list — a Lovable page cannot call them directly, with any credential. Proxy them through your server function and never return more to the page than the question to render.
- Resume is a header, and a resumed questionnaire cannot reach checkout. Send the enrollment id
back as
X-Journey-Idto continue a questionnaire on a return visit. A session resumed that way completes without adestination_capability, because naming an enrollment does not prove it is this patient’s. To take that patient to checkout, start a new questionnaire. - Which questionnaire is served follows your brand’s configured therapies. A brand with more than one enabled therapy must name an offering on the entry link so the therapy is unambiguous.
- Proof-of-identity capture is never part of this questionnaire. Questions collecting a government ID are withheld here and are not counted toward completion; the patient verifies identity in the member portal’s own identity step. See the identity handoff.
Path B — checkout first (only where you are configured for it)
This path needs no server of your own for the hand-off, but it works only on a checkout-first offering. On a questions-first offering, this link keeps the payment step locked and sends the patient to the health questions first, as described above.-
Present the offering. Read the price on your server with your API key and pass it to the
page, or read it in the browser with your publishable key once your origins are registered.
Either way send
X-Brand-Id— it is required, and with a publishable key it must equal the brand that key was minted for, or the read answers404. Key the buy control onprice_status, which every successful (200) storefront read carries:availableorpending. When it isavailable, the read carrieslist_price: thatamount_minoris the amount the checkout charges, and the one to show.pendingmeans a price is set but the billing terms are not, or the catalog was briefly unavailable, or the offering’s copy is written but its price is not set yet: show no amount and no buy control. Whenever the offering’s copy has been written, the read carries it incopy(display_name,description,includes), whatever the price state, so the page can show the offering’s words while its price is pending. An offering with neither copy nor a price does not answer200: the read answers404with typecommerce/merchandising/not-configured. Thepriceslist is display only (compare-at and introductory amounts). It may be empty, and it never sets what the patient is charged. The readiness block, including each offering’s activation status, is information for you. Do not show or hide the buy control on it. - Optionally, sign the patient in with the sign-in link above. The checkout signs the patient in itself if you do not.
-
Hand off with a link. Send the patient to
/checkouton your member portal address, carrying the offering:The parameters select what to present, never what anything costs. A link cannot name a price, a discount or a fee. A checkout-first link needs all three, and they must be one offering’s values exactly as your welcome pack and onboarding contact give them. Never mix thesku_idortherapyof one offering with theoffering_refof another. - The patient pays on the hosted page. Card details travel from the patient’s browser to the payment provider. Nothing payment-shaped passes through your code.
- The patient continues inside the portal — to the clinical questionnaire, then to their plan. There is no redirect back to your storefront, so your storefront is not where the outcome arrives. See below.
Never infer payment from the browser
There is no redirect back to your storefront after payment, and the outcome never rides the patient’s browser to reach you. A page that concludes “paid” because the patient returned, or because a timer elapsed, is guessing. Read the authoritative status from your server instead:GET /v1/payments/{order_ref}/status— the order’s ownpayment_state. Fed by confirmed events from the payment provider, never by guesswork. Branch on the wire values exactly as the reference declares them —created,payment_authorized,paid,payment_failed,partially_refunded,refunded,disputed,cancelled. Prose names like “authorized” or “failed” are not what the field carries, and a storefront matching on those never matches.POST /v1/webhooks/registrations— register an endpoint and the response returns your signing secret once. Deliveries carry an event type and identifiers only, so a delivery prompts a read rather than carrying detail.GET /v1/journeys/{journey_id}/status— the patient-visible enrollment status, for your own progress copy. It never carries payment detail, and it may answer with a typed no status yet instead of a status value.
Confirm your first test order
A test order is working when both of these hold:- The order is on the patient’s order page. Logged in as the test patient, open
https://{MEMBER_HOST}/order. The order is listed there. On a test brand whose clinical hand-off is not connected yet, the order reads “On hold on our side”. That is expected, and it still shows the order reached the portal. - Your server reads the order’s payment state. Take the order reference from the order’s row
on that page (each row shows it). No webhook delivery carries the order reference.
GET /v1/payments/{order_ref}/statusanswers with apayment_state. An order on hold may readpayment_authorized: the card is authorized but not yet charged. You do not needpaidto confirm a test order.
journey.payment.succeeded.v1 is not proof of a charge. It is not emitted from a
confirmed payment, so do not use it to decide that a patient has paid.
States your page must have
A generated app will happily render a price ofundefined. Ask for these explicitly.
The reusable prompt
Paste this into Lovable and fill the bracketed values. It contains no credentials and instructs the generated app never to hold one. It is written for questions-first mode; the checkout-first variant is marked.Development walkthrough checklist
- Read each offering’s entry mode and checkout link from your welcome pack.
- Store the API key in your server environment. Confirm it appears in no client bundle.
- Read the offering on your server and confirm
price_statusrenders, with the amount when it isavailable. - Sign in through
https://{MEMBER_HOST}/auth/login?returnTo=/homeand confirm you land in the member portal wearing your brand. - Build the hand-off for your entry mode and confirm it opens the hosted page wearing your brand. Checkout-first: the checkout link. Questions-first: register the origin your form page is served from, finish the questions, and confirm the form POST to the checkout door lands on the checkout within the 10-minute window.
- Place a test order as a logged-in, invented,
TEST-named patient, and confirm it the way Confirm your first test order describes: the order on the portal’s order page, and itspayment_stateread by your server, not the browser. - Only then, if you want the browser read: register your origins, ask for your publishable key, and move the price read client-side.
Common integration errors
Before you rely on any of this
Five things to confirm for your brand before you plan a launch around this page. None of them are code you write — they are configuration and credentials, and your onboarding contact owns them.- Your entry mode, for each offering, as your welcome pack shows it. On a questions-first offering, a link that carries no enrollment keeps the payment step locked.
- Your member portal address is set up. Without it there is no sign-in link and no checkout.
- Whether your publishable key is issued and your browser origins are registered. Until both are in place, the two browser reads are unavailable to your page and the server pattern above is your only route to a price. On a questions-first offering the origins matter even without a publishable key: the checkout door refuses a hand-off from an origin that is not registered.
- Whether your brand’s questionnaire is configured in tiers, if you want the split entry with
phase. Without tiers there is no qualification tier to serve on its own. - What your brand’s post-payment questionnaire currently serves. The platform owns that content; confirm its current shape before you design copy or a progress indicator around it.
TEST (see the warning at the top and
Environments) — and the prompt above is a starting point to verify in
your own project, not a certified integration. Run your own flow before you ship one.