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.
Code
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
customercreate payload (identical toPOST /customers), its required and defaulted fields, and the error codes — lives on thePOST /portal-sso/provisionoperation. 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
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)
- Minted just-in-time when
embed.jscalls yourfetchBindTokenprovider at checkout-start (the buy click or forced-checkout mount), not at page-load. Opaque, single-use, ~60-second TTL. - Delivered into the iframe over an origin-pinned
requestBindToken/bindTokenpostMessagehandshake. The message carries arequestIdso concurrent embeds can't cross-deliver. The ref never rides the iframe URL, referrer, or browser history. - 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. - 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.
- Expired, already used, or forged ref. The redeem treats it as a miss and the checkout proceeds anonymously (magic-link). No hard error.
- 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
fetchBindTokenprovider. - 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
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.