# 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](/guides/embedded-checkout/fulfillment#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`](/api/customers#list-customer-groups)),
alongside the expected `customerType`, `customerNumber`, `name`, and `countryCode`.
The [create-customer payload](/api/customers#create-a-customer)
is the source of truth for the full field list and defaults.

```bash
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](/api/customers#create-a-customer)
> (identical to `POST /customers`), its required and defaulted fields, and the
> error codes — lives on the
> [`POST /portal-sso/provision`](/api/portal-sso#provision-a-customer-grant-portal-access-partner-jit-provisioning)
> 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`:

```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>
<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](/guides/embedded-checkout/pricing-cards#target--authed-checkout-bind--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.

```html
<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](#2-register-a-fetchbindtoken-provider-on-your-page)).
The provider mints a ref against the just-created customer, the checkout binds, and
Details pre-fills.
