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, and a prompt you can reuse. Fortify is the worked example throughout. Substitute your own brand id and offerings.
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.

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:
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”.
  • 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.
The link to the hosted checkout needs no credential at all, which is why the direct path below works from a purely client-side app.

Development setup

Everything on this site targets the test-mode environment with test-scoped keys and synthetic data only — see environments.

Path A — checkout first, then the clinical questions

This is the path to build first, and the only one you can ship without a server of your own.
  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. Read price_status before you show an amount — pending means the terms are not fully set up and no amount is shown rather than a guessed one — and read the readiness block before you render a buy control.
  2. Hand off with a link. Send the patient to /checkout on your brand’s member origin, carrying the offering:
    The parameters select what to present, never what anything costs. A link cannot name a price, a discount or a fee.
  3. The patient signs in at the door. Checkout requires sign-in; the hosted page collects an email and runs the platform’s own login. Your storefront does not create the account.
  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.
Which way in is configuration, not a choice your link makes. Whether your program takes payment before the health questions is agreed when your program is set up. On a questions-first deployment a link with no enrollment attached shows the patient a plain “health questions come first” stop instead of a payment form. Confirm your brand’s configured mode with your onboarding contact before you build either path.
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.

Path B — qualification questions before checkout

Available, with one constraint that shapes the whole design: 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, and confirm your brand’s configuration first — see before you rely on any of this. 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. Then carry the journey_id to checkout:

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. 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. Send the enrollment id back as X-Journey-Id on a return visit rather than starting a second 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.

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.

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.

Development walkthrough checklist

  1. Confirm with your onboarding contact which entry path your brand is configured for.
  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 and the readiness block render.
  4. Build the checkout link and confirm it opens the hosted page wearing your brand.
  5. Complete a test-mode purchase and confirm your server — not the browser — observes the paid state through the payment status read or a webhook delivery.
  6. Confirm the patient lands in the member portal afterwards.
  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

Four 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. Which entry path your program is configured for. A questions-first program shows a plain “health questions come first” stop instead of a payment form when a link carries no enrollment.
  2. 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.
  3. 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.
  4. 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 live rails, no real card, no real order — and the prompt below is a starting point to verify in your own project, not a certified integration. Run your own flow before you ship one.