LedgerBee Developer
  • Getting started
  • Conventions
  • Products
  • API Reference
Subscriptions
Products & Pricing
Billing documents
WebhooksMCP (AI agents)
Customer Portal
    Portal SSO
    Embedded Checkout
      OverviewPricing cards & snippetLifecycle eventsBind to a customerGated plansFulfillment & errors
Embedded Checkout

Bind to a customer

Anonymous checkout (default)

With no partner binding, an anonymous buyer who completes checkout receives a magic-link verification email; clicking it finalizes the subscription. The buyer is identified by the email they enter. Payment is taken on the provider's hosted window via a redirect at verify time, so your origin is never in PCI scope.

Authed checkout — bind to a customer you already know (optional)

If you have already authenticated the end-customer on your side, bind the embedded checkout directly to that customer in LedgerBee, skipping the anonymous magic-link step. The buyer subscribes in one flow.

Security model: never put a raw customer id on the page — that is an IDOR. Your backend mints a short-lived, single-use, opaque checkout-bind ref. The embed requests it on demand from a provider you register on your page, carries only that ref, and LedgerBee resolves the customer from it server-side.

1. Your backend mints a checkout-bind token

The LedgerBee public API base URL is https://api.ledgerbee.com/api/v1; the exact host is confirmed when your API key is issued. Authenticate with a public API key that carries the portal-provision and portal-sso-mint scopes. Pass the key in the x-api-key request header, not Authorization: Bearer — the API reserves Bearer for OAuth access tokens and rejects a raw key sent that way. The call ensures the customer's portal user and membership exist and returns the bind token in one round trip.

Identify the customer by customerId (an existing customer) or a customer object that upserts by its customerNumber (reuse-or-create). Supply exactly one. Set mintCheckoutBindToken: true to get the bind token back. Optionally pass your own clientReferenceId (your order/cart id) — it rides the bind ref onto the resulting subscription and into every later subscription webhook so you can reconcile (see Fulfillment & reconciliation).

The top-level email is required and is the portal-user identity to grant access to: a credential-less portal user is created (or reused) for that email and given an active membership — it's the login the buyer later uses to manage the subscription. It is distinct from customer.email (the customer record's contact email); when you send a customer object and omit customer.email, this top-level email is used as the customer's contact email too.

When you create a customer (the customer object, not customerId), the payload is the same shape as POST /customers and rejects with raw validation errors if a required field is missing. The non-obvious required fields are vatZone (a VATZone enum — domestic, eu, abroad, or domestic_without_vat, all lowercase) and customerGroupId (a uuid — list your tenant's groups via GET /v1/customers/groups), alongside the expected customerType, customerNumber, name, and countryCode. The create-customer payload is the source of truth for the full field list and defaults.

TerminalCode
curl -X POST https://api.ledgerbee.com/api/v1/portal-sso/provision \ -H "x-api-key: $LEDGERBEE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "customerId": "550e8400-e29b-41d4-a716-446655440010", "email": "jane.doe@acme.example", "mintCheckoutBindToken": true, "clientReferenceId": "order_7f3a9c21" }'

The response returns checkoutBindToken and checkoutBindExpiresAt. The call is idempotent — an existing customer is reused as-is, never mutated — so it is safe to make on every checkout. clientReferenceId is optional, server-to-server only (it never reaches the iframe), max 200 chars, [A-Za-z0-9_-].

The request/response schema is defined in the published LedgerBee Public API reference, not here. Every field — including the customer create payload (identical to POST /customers), its required and defaulted fields, and the error codes — lives on the POST /portal-sso/provision operation. That spec is the source of truth; this guide covers only how the call fits into the embed flow.

2. Register a fetchBindToken provider on your page

Rather than putting the ref on the iframe, register an async provider on a host-page global. embed.js retains the reference and invokes it on demand at checkout-start — when the buyer starts a checkout, not on page load. The iframe keeps only data-ledgerbee-pricing:

Code
<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> <script> window.LedgerBee = window.LedgerBee || {}; window.LedgerBee.embed = { // () => Promise<string | null | undefined> fetchBindToken: async () => { const r = await fetch('/your-backend/bind-token', { method: 'POST' }); if (!r.ok) return null; // → anonymous checkout const { checkoutBindToken } = await r.json(); // your backend's provision proxy return checkoutBindToken; // a FRESH ref, per call }, }; </script>

Your /your-backend/bind-token endpoint is a thin proxy that calls POST /v1/portal-sso/provision with mintCheckoutBindToken: true (step 1) and returns the fresh checkoutBindToken. Mint a new ref per call; do not cache the ref. You may cache the resolved customerId and pass it back on subsequent calls, since the provision call is idempotent.

Token lifecycle (on demand — always fresh)

  1. Minted just-in-time when embed.js calls your fetchBindToken provider at checkout-start (the buy click or forced-checkout mount), not at page-load. Opaque, single-use, ~60-second TTL.
  2. Delivered into the iframe over an origin-pinned requestBindToken / bindToken postMessage handshake. The message carries a requestId so concurrent embeds can't cross-deliver. The ref never rides the iframe URL, referrer, or browser history.
  3. Redeemed atomically at POST /api/checkout/session (the embed iframe calls this internally — you never call it directly): peek tenant-check, signed-in-customer check, and single-use consume, binding the session to your customer in place. No intermediate handle, no claim step.
  4. Every checkout start mints a new ref. The mint-to-redeem window is seconds — the provider call and the session mint happen on the same checkout-start. A buyer who dwells on the checkout step can let a ref age past its 60s TTL, in which case the redeem degrades to anonymous; re-start checkout for a fresh ref.
  5. Expired, already used, or forged ref. The redeem treats it as a miss and the checkout proceeds anonymously (magic-link). No hard error.
  6. Wrong tenant or wrong intent. A ref minted for another tenant, or a login (SSO handoff) ref, is rejected.

A signed-in portal session takes precedence. If the buyer is already authenticated as a portal user and your provider hands back a ref for a different customer, the bind is rejected — don't hand an authenticated buyer a foreign-customer ref. Same-customer is harmless.

Guarantees

  • Authed bind works on every surface. In-frame binds directly; a breakout binds via the session-locator redirect (see the support matrix). The only requirement is a registered fetchBindToken provider.
  • The bind ref is single-use, tenant-scoped, and redeemed in place at session mint. The iframe clears it after the session is created, so a still-valid ref never lingers in client JS.
  • Card capture stays on the provider's hosted window. Your origin and LedgerBee's embed never see raw card data (PCI SAQ A).

What the buyer sees — bound vs anonymous Details step

A bound checkout pre-fills the Details step read-only from the customer you provisioned: name, email, VAT, and address come from the LedgerBee customer record. Only the start date stays editable. The bind subscribes that exact customer; to change those fields, correct the customer record operator-side.

An anonymous checkout shows the same Details step editable and empty. The buyer fills it in, and confirm stages a magic-link signup keyed on the email they enter.

To tell whether a binding worked end-to-end: the Details step arrives pre-filled and read-only. If it is editable, the checkout is on the anonymous path — your fetchBindToken provider returned null or wasn't registered, the ref expired between mint and checkout-start, or it was for the wrong tenant or intent.

Defer the buy action to your page (signup-first funnel)

To show anonymous pricing cards on a marketing page and route the buy click to your own signup first — then drop the buyer into the bound checkout once the customer exists — use target=callback. The buy click fires checkoutInitiated and opens nothing, so your listener owns the action.

Code
<iframe src="https://<slug>.portal.ledgerbee.com/embed/<vanity>?target=callback" data-ledgerbee-pricing loading="lazy" style="width:100%;border:0"></iframe> <script src="https://<slug>.portal.ledgerbee.com/embed.js" async></script> <script> window.addEventListener('ledgerbee:embed:checkoutInitiated', (e) => { const { vanity, planItemId } = e.detail; // ids + vanity only — no PII sessionStorage.setItem('lb.resume', JSON.stringify({ vanity, planItemId })); window.location.href = '/signup?next=checkout'; // your own funnel }); </script>

After your signup completes and the customer exists in your tenant, send the buyer to the forced single-item checkout for the stored item, …/embed/checkout/<vanity>/<planItemId>, on a page that registers a fetchBindToken provider (step 2). The provider mints a ref against the just-created customer, the checkout binds, and Details pre-fills.

Last modified on June 14, 2026
Lifecycle eventsGated plans
On this page
  • Anonymous checkout (default)
  • Authed checkout — bind to a customer you already know (optional)
    • 1. Your backend mints a checkout-bind token
    • 2. Register a fetchBindToken provider on your page
    • Token lifecycle (on demand — always fresh)
    • Guarantees
    • What the buyer sees — bound vs anonymous Details step
  • Defer the buy action to your page (signup-first funnel)