LedgerBee Developer
  • Getting started
  • Conventions
  • Products
  • Configuration
  • API Reference
Subscriptions
Payment flowCard paymentsProducts & PricingProduct Entitlements
Billing documents
Accounting
WebhooksPartner-Hosted Checkout
Customer Portal
    Portal SSO
    Embedded Checkout
      OverviewPricing cards & snippetLifecycle eventsBind to a customerGated plansFulfillment & errors
Embedded Checkout

Fulfillment & errors

Fulfillment & reconciliation

Fulfill and reconcile through LedgerBee webhooks, not the client redirect or the checkoutConfirmed browser event. The bound subscription fires subscription.started / subscription.assigned (and every later subscription webhook) carrying partnerReferenceId — the clientReferenceId you passed at provision — so you can match our subscription to your order. Subscribe to those topics and key your fulfillment on partnerReferenceId. See Webhooks for registering an endpoint, verifying the Standard-Webhooks signature, and the delivery-envelope shape.

Recover a missed webhook. If a delivery is lost (your endpoint was down, you never subscribed yet, a backfill), resolve our subscription from your own order id over the public API: GET /v1/subscriptions?partnerReferenceId=<your clientReferenceId>. The subscription read responses now carry partnerReferenceId, and the list accepts it as a filter — so the join key is reachable without replaying the webhook. (Requires the subscriptions-read scope; see the list-subscriptions operation.)

Anonymous purchases and in-portal changes. A purely anonymous (magic-link) purchase still fires the same subscription webhooks, but with partnerReferenceId: null — you get the customerId and the stable subscriptionId, not your own order id. To tie a purchase back to your record, pass a clientReferenceId when you mint the token — either bound or vouched. A vouched checkout also echoes it on the customer-created webhook (see event types), so a customer created at confirm carries your order id from the moment it exists. An upgrade or downgrade the buyer makes inside the portal arrives as a structural-change subscription webhook (transitionType: plan_replaced) on that same stable subscriptionId — see the event types — so you follow one subscription across plan changes.

The ledgerbee:embed:checkoutConfirmed browser event AND the returnUrl redirect are UX-only — use them to remove the iframe, show a thank-you, or hand the buyer back to your app, never as a fulfillment trigger. The webhook is the source of truth. Three reasons:

  • The browser event can be lost. A browser that closes mid-redirect drops it, and nothing replays it.
  • ?checkout=pending means "inbox sent", not "subscribed". An anonymous checkout has only mailed a magic link at that point; the subscription does not exist until the buyer clicks it.
  • ?subscriptionId= arrives only on CONFIRMED. For an anonymous buyer that is a later, separate call to your page, after they verify — see when your page is called for which rails call you once and which call you twice.

Errors & failure states

SituationWhat you observeWhat to do
Plan not public yetthe /embed/<vanity> iframe shows a "not found" stateoperator publishes the plan + allows it via a portal routing rule
Your origin not allowlistedthe browser refuses to frame the embed (CSP frame-ancestors); a console error, and no events fireoperator adds your site's origin to the partner-origin allowlist (Settings → Portal → Embedding)
Tenant lacks the CustomerPortal licenseresolve and the catalogue reads still answer — checkout is license-free. provision returns HTTP 403, body { "code": "LICENSE.REQUIRED" } (with details.licenseKey)the tenant must hold the CustomerPortal license to provision portal access
Provision: missing scopeHTTP 403, body { "code": "INSUFFICIENT_PERMISSIONS" }the API key needs portal-provision (+ portal-sso-mint for the token)
Provision: unknown customerIdHTTP 404, body { "code": "CUSTOMER_NOT_FOUND" }the customerId must reference an existing customer in your tenant; this call never creates one from an id (send a customer object to create)
Bind ref expired / reused / no providerthe embed proceeds with the anonymous (magic-link) checkout; no hard errorregister a fetchBindToken provider that mints a fresh ref per call; if a buyer dwells past the ~60s TTL, re-starting checkout mints a new one
Bind ref for the wrong tenant, or for a different customer than a signed-in buyerPOST /api/checkout/session returns 400 PORTAL_CHECKOUT_SESSION_INVALID (reason: TENANT_OR_ITEM_MISMATCH); the embed bounces back to the cards and fires ledgerbee:embed:checkoutErrora security reject; don't hand a checkout a foreign-tenant ref, or an authenticated buyer a foreign-customer ref
Checkout session expired (30-min TTL) or already consumedconfirm / resume returns 410 PORTAL_CHECKOUT_SESSION_INVALID (reason: EXPIRED_OR_NOT_FOUND)re-start checkout for a fresh session — distinct from a forged id (400)
Duplicate confirm (double-click, network retry)409 PORTAL_CHECKOUT_IN_PROGRESSthe first submit is being processed; don't resubmit — exactly one subscription is created
Buyer abandonsnothing is persistedserver-side checkout sessions expire on their own (30 min)

Operator prerequisites (configured in LedgerBee → Settings → Portal → Embedding)

  • The plan has a public vanity URL and is published + allowed by a routing rule.
  • Your site's origin(s) are on the tenant's partner-origin allowlist. This drives the frame-ancestors policy; the embed refuses to frame on an un-allowlisted origin. Allowlisting is the only LedgerBee setup on your side. Loading the iframe and exchanging postMessage events with it are not governed by CORS, and the checkout's own requests run from inside the frame to the portal API, whose policy already permits the portal origin — nothing there needs configuring by you. Requests your own page makes directly, such as the fetchBindToken provider calling your backend, follow ordinary CORS rules and are yours to configure. A development server may be allowlisted over plain http, for the hosts localhost, 127.0.0.1, and [::1] only.
  • "Allow in-frame checkout" is on if you want target=inline or any item-pinned checkout.
Last modified on September 13, 2026
Gated plans
On this page
  • Fulfillment & reconciliation
  • Errors & failure states
  • Operator prerequisites (configured in LedgerBee → Settings → Portal → Embedding)