Embedded Checkout
Embed your pricing cards and checkout directly onto your own site. The cards
render in a sandboxed iframe served from your portal origin
(https://<slug>.portal.ledgerbee.com). Checkout runs in-frame or breaks out to
a new tab. Card capture stays on the payment provider's hosted window, so your
page never touches card data.
Placeholders: <slug> is your tenant subdomain. <vanity> is a plan's public
vanity slug — the same slug as its /p/<vanity> page (e.g. pro-plan), never a
plan UUID. <planItemId> is a specific item's id, required for the item-pinned
and forced-checkout surfaces (see Three surfaces). All three appear in the
operator UI at Settings → Portal → Embedding, which generates a ready-made
snippet with the ids filled in. Copy that snippet rather than hand-building the
URL.
This guide is split across pages: Pricing cards & snippet, Lifecycle events, Bind to a customer, Gated (non-public) plans, and Fulfillment & errors.
When a customer account & subscription are created
Embedding or displaying pricing never creates a customer or subscription. Both
the public cards and the gated cards resolved for a specific buyer are read-only:
the catalogue reads and
POST /v1/portal/plans/resolve run the
visibility rules and (for a gated buyer) mint a short-lived, read-only display
token, but write nothing to your ledger. Resolving with a customerId reads an
existing customer; resolving with countryCode / customerType hints reads no
customer at all. A visitor who is shown cards and leaves persists nothing.
Records are created only at checkout, and the timing depends on the flow:
| Flow | When the customer + subscription are created |
|---|---|
| Anonymous checkout (no binding) | When the buyer completes checkout and clicks the magic-link verification email. Submitting the form only stages a self-expiring pending sign-up and sends the email; the subscription does not exist until the link is clicked. |
| Bound checkout (you provision) | The customer is created (or reused) by your POST /v1/portal-sso/provision call; the subscription is created when the buyer confirms the bound checkout. No magic-link verification. |
A buyer who abandons before these points leaves no customer or subscription — only the server-side checkout session, which expires on its own (30 min). Any card captured before abandonment lives on the payment provider, not your ledger, and is cleaned up automatically.
Skipping the magic-link verification
The magic-link step exists only on the anonymous flow; it is how an anonymous buyer's record is deferred until they prove their email. To skip it, use the bound flow: provision the customer and bind the checkout, and the buyer subscribes in one step with no email round-trip.
Binding is decided when the checkout session is created — at the buy click or
forced-checkout mount, via your fetchBindToken provider — not at confirm. The
provision call therefore runs before the bound checkout, and there is no path to
let a buyer confirm anonymously and then convert the in-progress checkout to a
bound one. For a buyer you already know (a signed-in user of your platform),
provision at the buy click and run the bound checkout directly; the
signup-first funnel
(target=callback) catches the buy click, provisions on the buyer's behalf, then
drops them into the bound checkout.
Skipping verification is a trade-off: the customer record is then created at the
buy click rather than only on completion, so a buyer who abandons a bound
checkout leaves a customer with no subscription. provision is idempotent, so
you control when — and whether — to create it.
Three surfaces (same embed.js)
These three surfaces address a plan by its public <vanity> slug, so they
require a public plan (published + a vanity URL + an allow-all routing rule).
To embed a non-public plan for a buyer you've already identified, use the
by-id surfaces in Gated (non-public) plans
instead.
Checkout always targets one item. There is no standalone vanity-only checkout — that is the pricing card. The three surfaces are:
| Surface | URL | Shows | Use when |
|---|---|---|---|
| Pricing card | https://<slug>.portal.ledgerbee.com/embed/<vanity> | the plan's card(s) | The buyer compares options; the buy CTA starts the chosen item's checkout. |
| Item checkout — keeps cards | https://<slug>.portal.ledgerbee.com/embed/<vanity>?item=<planItemId> | that item's checkout, with the cards loaded behind it | You drop the buyer straight on one item but still let them go Back to the full card set. Always in-frame. |
| Forced single-item checkout | https://<slug>.portal.ledgerbee.com/embed/checkout/<vanity>/<planItemId> | that item's checkout only | You want one item, no cards and no Back. Always in-frame. |
All three carry the data-ledgerbee-pricing attribute so embed.js manages them
(resize plus the on-demand bind handshake). Every surface requests a fresh bind
ref at checkout-start, not on load — see the
token lifecycle.
The first path segment is the plan's vanity slug, not a UUID.
<planItemId>is a server-side id. Both are filled in by the operator's snippet generator at Settings → Portal → Embedding — pick a snippet type (Pricing cards, Checkout (with cards), or Checkout (item only)) and copy the result rather than constructing the URL by hand.
Quick start (pricing card)
Code
- The companion
embed.jsis optional but recommended. It auto-resizes the iframe to the content height (no inner scrollbar), re-dispatches lifecycle events as DOM events on your page, and performs the bind-token handshake. It carries no tenant or plan knowledge; one copy serves every embed on the page. - The embed renders only once the plan is publicly reachable — the operator must publish it and allow it via a portal routing rule. Otherwise the iframe shows a "not found" state. See Errors.