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

Pricing cards & snippet

A pricing card embeds a plan's tiers — each tier's title, price, cadence selector, feature list, and a buy button — in a sandboxed iframe on your own page. It is the /embed/<vanity> surface, addressed by the plan's public vanity slug.

The snippet

Drop the iframe plus embed.js on your page. embed.js auto-resizes the frame and runs the bind / catalog handshakes; the cards render from the tenant's portal origin.

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>

Generate it with the ids filled in at Settings → Portal → Embedding (snippet type Pricing cards) rather than hand-building the URL. <vanity> is the plan's public slug; the query parameters below tune language, theme, and where the buy CTA opens.

What the buyer sees

The public pricing card is anonymous. Every visitor sees the same cards and the same buy CTA, and clicking it starts checkout — an anonymous magic-link checkout, or a bound checkout when your page registers a fetchBindToken provider (see Bind to a customer). The card carries no buyer identity, so it never shows a current plan or upgrade/downgrade options.

To show View details / Upgrade / Downgrade in place of the buy CTA — for a buyer you have already identified — embed the gated by-id card and resolve it with that buyer's customerId. The customer-bound catalog token lets the card resolve the buyer's owned tiers and render the affordances the logged-in portal shows. See Gated (non-public) plans → Returning buyers.

Deep-link to one item

Drop the buyer straight onto one tier's checkout while keeping the cards behind it (Back returns to them) by adding ?item=<planItemId> to the card URL. Always in-frame.

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

Forced single-item checkout

Show only one tier's checkout — no cards, no Back — on the dedicated checkout route. Always in-frame; needs the operator's "Allow in-frame checkout" on.

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

Custom call-to-action cards ("talk to sales")

A plan's card grid can include a custom call-to-action card — the operator's "talk to sales" or Enterprise tier. In the catalogue it is the item whose itemType is "CUSTOM_CTA". It renders as a normal pricing card, with its own icon, features, and button label, and is visually indistinguishable from a priced tier. It shows no price, its button navigates to an operator-set link instead of starting checkout, and it always renders after every priced card in the grid.

The link (ctaLink) is an absolute https:// URL, a site-relative path (/contact), a mailto: address, or a tel: number. Clicking the button does not start checkout and emits no checkout lifecycle event — there is no checkoutInitiated or checkoutConfirmed, since it is not a checkout. The button respects the iframe target param: target=_top navigates your host page to the link; any other target opens the link in a new tab so your embedding page stays in place. A mailto: or tel: link always hands off to the OS handler.

Set ctaCallback=1 on the iframe to make a custom CTA button hand the click to your page instead of following its link. The button then fires the customCtaClicked event and navigates nothing, so your page owns the action — open a chat widget, a contact modal, or a calendar. The event carries the operator ctaLink, which your handler can navigate to or ignore. This is independent of target, so a host can intercept CTA buttons while still using any checkout mode.

A CUSTOM_CTA item in the backend catalogue read carries itemType: "CUSTOM_CTA", its ctaLink, a ctaPresetKey ("contactUs", "bookDemo", or "scheduleTime", derived from the button label for analytics), and priceMinor: null with sourceSubscriptionId: null and addons: []. Treat these items as non-purchasable wherever your code expects a buyable card.

When you set ctaCallback=1, register a listener so your page owns the click:

Code
<script> window.addEventListener('ledgerbee:embed:customCtaClicked', function (e) { var d = e.detail; // { vanity, planItemId, ctaLink, frame } — no PII // Open your own chat widget / contact modal here, or navigate yourself: // if (d.ctaLink) window.location.href = d.ctaLink; window.openMyChatWidget && window.openMyChatWidget(); }); </script>

The full e.detail payload (and every other embed event) is in Lifecycle events.

Snippet query parameters

ParamValuesDefaultMeaning
langen | daenAll surfaces. Language the cards + checkout render in.
themesystem | light | darksystemAll surfaces. Color theme. system follows the visitor's OS preference.
targetinline | _blank | _top | callbackinlinePricing card only. Where the buy CTA opens checkout (see matrix below). callback opens nothing: the click fires checkoutInitiated and your page owns the action (signup-first funnel; see Defer the buy action). The two item-pinned surfaces are always in-frame and ignore it.
itema <planItemId>(none)Pricing card only. Deep-links the pricing-card embed straight to one item's in-frame checkout while keeping the cards behind it; Back returns to them. For a checkout with no cards, use the path-segment form …/embed/checkout/<vanity>/<planItemId> instead of this query param. Either id comes from the operator's snippet; it is a server-side id, not a value you construct.
ctaCallback1(off)Pricing card only. Defers custom CTA card clicks to your page: the button fires customCtaClicked and navigates nothing, instead of following its ctaLink. Independent of target. Has no effect on priced cards.

The buy CTA starts checkout on every surface. The one exception is target=callback, where your page owns the action.

Change only the query string to switch breakout behaviour or theme — the rest of the snippet is identical:

Code
<!-- New tab on buy, dark theme --> <iframe src="https://<slug>.portal.ledgerbee.com/embed/<vanity>?lang=en&target=_blank&theme=dark" 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>

target × authed checkout-bind — support matrix

The authed checkout-bind works on every surface, in-frame and breakout. An in-frame checkout binds directly. A breakout (_blank / _top) binds via a session-locator redirect: the iframe mints a bound session server-side, then redirects the new tab or host page with only the opaque session id in the URL fragment. With callback, binding happens on the resume surface you send the buyer to.

targetWhere checkout runsBind honored?
inline (+ operator "Allow in-frame checkout" on)in the iframeyes (in-frame)
inline (toggle off)falls back to a new tab (standalone)yes (via the session-locator redirect)
_blanknew tab (standalone)yes (via the session-locator redirect)
_tophost page navigates (standalone)yes (via the session-locator redirect)
callbacknothing opens — your page owns itN/A — bind on the resume surface you build

Binding requires only that your page register a fetchBindToken provider (see Bind to a customer). With no provider the buyer goes through the anonymous flow on every mode.

Breakout binding (new tab / redirect)

A breakout binds by creating the bound session server-side first. On a _blank / _top buy click the iframe:

  1. Requests a fresh bind ref from your fetchBindToken provider (same on-demand handshake as in-frame). No provider or a null ref produces an anonymous breakout to the plain …/checkout/<…> URL.
  2. Mints a bound session server-side and gets back an opaque sessionId.
  3. Redirects the breakout to …/checkout/<planVersionId>/<planItemId>#session=<sessionId> — the session locator in the URL fragment, never the bind ref and never a query param, so it is not sent to any server and not in Referer.

The standalone checkout page reads the #session= fragment, strips it from the URL immediately, and rehydrates the bound session (its quote plus the read-only Details pre-fill). The locator is single-use (consumed at confirm), tenant-scoped, and stripped on arrival. The locator expires after a short TTL; anyone holding the checkout URL can complete checkout until then.

Return to your page after checkout

returnUrl sends the buyer to a page of yours once checkout completes. Put it on the embed URL, or on a plain checkout link you hand out yourself:

Code
<iframe src="https://<slug>.portal.ledgerbee.com/embed/<vanity>?lang=en&target=_blank&theme=system&returnUrl=https://your-site.example/thanks" 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>
Code
https://<slug>.portal.ledgerbee.com/checkout/<planVersionId>/<planItemId>?returnUrl=https%3A%2F%2Fyour-site.example%2Fthanks

The redirect happens only when the URL's origin is on the tenant's embeddable-origins allowlist, so add the origin at Settings → Portal → Embedding before the link will do anything. An origin that is not on the list leaves the buyer on the LedgerBee confirmation panel. The snippet generator on that same screen produces the URL-encoded value.

When your page is called

Checkout finishes at a different moment on each rail, and the redirect follows that moment rather than the submit.

RailYour page is called
Signed-in buyer, invoice, checkout in its own tab or pageOnce, on submit, with ?subscriptionId=.
Signed-in buyer, invoice, checkout inside the iframeNever — your page is still on screen behind the frame, which reports checkoutConfirmed instead.
Anonymous buyer, checkout in its own tab or pageTwice — ?checkout=pending on submit, then ?subscriptionId= when the buyer clicks the verification email.
Anonymous buyer, checkout inside the iframeOnce, when the buyer clicks the verification email, with ?subscriptionId=.
Card paymentOnce, after the charge clears, with ?subscriptionId=.

What splits each pair of rows is where the submit happens, and it is the same split on both. A checkout running inside the frame never navigates your page on submit — it reports checkoutConfirmed and shows "check your inbox" in the frame, so treat that event as the pending signal and expect exactly one call, later. A checkout opened in its own tab or page has your page to return to on submit, so it lands there with ?checkout=pending.

Both then converge: an anonymous checkout creates nothing on submit, and the subscription exists only once the buyer clicks the link in the verification email. That click opens a top-level page — outside any frame, possibly on another device and hours later — and it is the call that carries ?subscriptionId=. Treat it as the completion.

A card checkout redirects only once the charge succeeds, so your page is never reached for an unpaid checkout. A declined card leaves the buyer in the payment step to retry.

What arrives on the URL

ParameterMeans
?subscriptionId=<id>The subscription exists.
?checkout=pendingA verification email was sent. Nothing exists yet.

Query parameters you put on the URL yourself are preserved alongside these.

The redirect fires whenever the buy button opens the standalone checkout. That is always the case for _blank and _top, and it is also where inline lands when in-frame checkout is off, and where callback lands when the page turns out not to be framed. Set returnUrl on any of them and it is used if that mode breaks out.

Last modified on November 6, 2026
OverviewLifecycle events
On this page
  • The snippet
  • What the buyer sees
  • Deep-link to one item
  • Forced single-item checkout
  • Custom call-to-action cards ("talk to sales")
  • Snippet query parameters
    • target × authed checkout-bind — support matrix
    • Breakout binding (new tab / redirect)
    • Return to your page after checkout