Skip to main content
Lovable generates a web app from a written prompt. That makes it a fast way to stand up a branded storefront in front of this platform — and it makes one mistake unusually easy, because a prompt is pasted whole and the generated code runs in your patient’s browser. This page is written for that: what your storefront owns, what the platform hosts, which credential may ever reach client code, how to confirm your first test order, and a prompt you can reuse. The examples use the brand id 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.
Read this before you paste anything into a prompt. On this platform, an API key beginning pk_ is a server secret. That is the opposite of the convention some payment tools use, where a pk_ value is the browser-safe one. The browser-safe credential here begins pub_test_ or pub_live_.A pk_ value pasted into a Lovable prompt, a client component, or a public repository is a disclosed server credential and must be rotated — and rotation is bounded, not instant: the old value can still be accepted for up to 30 seconds afterwards. See your credentials.
Use only invented test patients whose names include the uppercase word TEST; test mode never charges a real card.

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: Everything else in this guide — starting a questionnaire, reading a payment status, reading an enrollment status, registering a webhook endpoint — needs the API key and therefore needs a server.

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).
Three properties that matter to a Lovable project specifically:
  • 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 http is accepted only for a loopback host (localhost, 127.0.0.1, [::1]), so a local preview can use http://localhost:5173; every other origin must be https.
  • 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:
This is the member portal’s own sign-in route, and it gives the patient a member portal session. 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. (The checkout’s own sign-in step uses the same portal route, so a patient who arrives logged out signs in there before paying.) So signing in is required, and the checkout handles it itself: payment stays locked until the patient is logged in. Your storefront’s own sign-in step is optional. Your storefront cannot tell whether a patient already has a member portal session, because that session lives on the member portal address, not on yours, so show the sign-in link to everyone. Do not link patients to the API’s /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.
Always send 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 carrying journey_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-Id header, 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).
The value is a credential. Keep it on your server. Never put it in a URL or a log, and never return it to your page’s JavaScript as data. If the completing response does not carry it, this enrollment cannot be handed to checkout: start a new questionnaire. 2. Send the patient to the checkout door with a top-level form POST. When the questions complete, navigate the browser to a route on your server, with a full page load rather than a fetch. That route returns a page holding a form that posts to your member portal’s checkout door. The pass goes in the request body, in the capability field, with your offering_ref beside it:
The script submits the form as soon as the page loads, and the button covers a browser that runs no script. The form carries no 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.
Not yet verified on Lovable hosting. This step assumes your server function can do two things: return the HTML page above as a full page, and keep the pass on the server for this visitor (for example, behind an httpOnly cookie on that host). Neither has been tested on Lovable’s hosting yet. The door’s side of the hand-off is unaffected. Confirm both in your own project before you rely on this pattern.
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:
Serving a phase requires a questionnaire configured in tiers; an unknown value is rejected with 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-Id to continue a questionnaire on a return visit. A session resumed that way completes without a destination_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.
  1. 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 answers 404. Key the buy control on price_status, which every successful (200) storefront read carries: available or pending. When it is available, the read carries list_price: that amount_minor is the amount the checkout charges, and the one to show. pending means 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 in copy (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 answer 200: the read answers 404 with type commerce/merchandising/not-configured. The prices list 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.
  2. Optionally, sign the patient in with the sign-in link above. The checkout signs the patient in itself if you do not.
  3. Hand off with a link. Send the patient to /checkout on 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 the sku_id or therapy of one offering with the offering_ref of another.
  4. 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.
  5. 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.
What you own on this path, and where it ends. The storefront, the hand-off link and the reads your server makes afterwards are yours to build now. Everything from the checkout page onward — the payment, the questionnaire the patient meets next, the clinician review — is served by the platform inside the portal, and its content is configuration for your program rather than something your storefront drives. Confirm what your brand’s post-payment questionnaire currently serves before you write copy or a progress indicator that depends on its shape.

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 own payment_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.
A failed payment still leaves a readable order — the attempt is never invisible.

Confirm your first test order

A test order is working when both of these hold:
  1. 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.
  2. 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}/status answers with a payment_state. An order on hold may read payment_authorized: the card is authorized but not yet charged. You do not need paid to confirm a test order.
The event 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 of undefined. 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

  1. Read each offering’s entry mode and checkout link from your welcome pack.
  2. Store the API key in your server environment. Confirm it appears in no client bundle.
  3. Read the offering on your server and confirm price_status renders, with the amount when it is available.
  4. Sign in through https://{MEMBER_HOST}/auth/login?returnTo=/home and confirm you land in the member portal wearing your brand.
  5. 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.
  6. 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 its payment_state read by your server, not the browser.
  7. 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.
  1. 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.
  2. Your member portal address is set up. Without it there is no sign-in link and no checkout.
  3. 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.
  4. 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.
  5. 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.
And two standing facts about this site’s environment: every example runs in test mode — no real card is ever charged, and you use only invented test patients whose names include the uppercase word 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.