# Embedded Checkout


> Embed a tenant's pricing cards and checkout into your own site. The cards
> render in a sandboxed iframe served from the tenant's 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](/guides/embedded-checkout/pricing-cards),
[Lifecycle events](/guides/embedded-checkout/lifecycle-events),
[Bind to a customer](/guides/embedded-checkout/customer-binding),
[Gated (non-public) plans](/guides/embedded-checkout/gated-plans), and
[Fulfillment & errors](/guides/embedded-checkout/fulfillment).

## 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](/guides/embedded-checkout/gated-plans)
instead.

Checkout always targets one item. There is no standalone vanity-only checkout —
that is the pricing card. The three surfaces are:

<div className="lb-surface-table">

| 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. |

</div>

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](/guides/embedded-checkout/customer-binding#token-lifecycle-on-demand--always-fresh).

> The first path segment is the plan's vanity slug, not a UUID. `<planItemId>` is
> a server-side id; do not guess it. 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)

```html
<iframe
  src="https://<slug>.portal.ledgerbee.com/embed/<vanity>?lang=en&target=inline&theme=system"
  data-ledgerbee-pricing
  loading="lazy"
  style="width:100%;border:0;min-height:520px"
  title="Pricing"></iframe>
<script src="https://<slug>.portal.ledgerbee.com/embed.js" async></script>
```

- The companion `embed.js` is 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](/guides/embedded-checkout/fulfillment#errors--failure-states).
