# Lifecycle events

`embed.js` validates each message's origin against the iframe it owns, then
re-dispatches these as `CustomEvent`s on your `window`. Wire a handler with
`addEventListener`:

```js
window.addEventListener('ledgerbee:embed:checkoutConfirmed', (e) => {
  const { planVersionId, planItemId, status, frame } = e.detail;
  if (status === 'CONFIRMED') frame.remove(); // swap the frame for your thank-you
});
```

Every event's `detail` carries the fields listed below plus `frame`, the source
`<iframe>` element, so a page with several embeds can tell which one fired. These
events carry ids, the plan slug, and a coarse status only; no email or other PII
crosses the iframe boundary.

{/* @codegen embed-lifecycle-events — generated by `pnpm docs:generate`; do not edit by hand */}

## `ledgerbee:embed:pageLoaded`

The cards finished their first data-driven paint. Reveal the frame or drop your loading skeleton.

_No payload beyond `frame`._

## `ledgerbee:embed:checkoutInitiated`

The visitor clicked a buy CTA, fired just before the break-out to checkout. In `target=callback` mode it fires instead of opening anything, so your listener owns the buy action (e.g. route to your signup funnel first, then resume into the bound checkout).

| Field | Description |
|---|---|
| `planVersionId` | The plan version being purchased. |
| `planItemId` | The chosen item within the plan. |
| `vanity` | The plan's public vanity slug — build the resume URL `…/embed/checkout/<vanity>/<planItemId>` straight from it. |
| `deferred` | `true` only in `target=callback` mode, where the iframe opened nothing and your listener owns the action; absent otherwise. |

## `ledgerbee:embed:customCtaClicked`

The visitor clicked a custom CTA card's button (a "talk to sales" / "book a demo" card, not a checkout). Fired only when the embed was generated with `ctaCallback=1`: the iframe navigates nothing and hands the click to your page.

| Field | Description |
|---|---|
| `planVersionId` | The plan version the card belongs to. |
| `planItemId` | The card item that was clicked. |
| `vanity` | The plan's public slug, so you can correlate which embed fired. |
| `ctaLink` | The operator-configured link the card would otherwise have followed (absolute URL, relative path, `mailto:`, `tel:`), or `null` if unset. Navigate to it yourself, or open your own chat / contact UI instead. |

## `ledgerbee:embed:checkoutConfirmed`

The in-frame checkout finished — swap the frame for your own thank-you or remove the iframe. A bound OnPay checkout does not fire this (it breaks out to OnPay to take payment first); reconcile that path via webhooks instead.

| Field | Description |
|---|---|
| `planVersionId` | The plan version that was purchased. |
| `planItemId` | The item that was purchased. |
| `status` | How the in-frame flow finished: `CONFIRMED` (subscribed directly — safe to remove the frame), `PENDING_VERIFICATION` (anonymous buyer, magic link sent — keep the frame so they can read "check your inbox"). |

## `ledgerbee:embed:checkoutError`

An in-frame checkout attempt failed. The buyer still sees the actionable message inside the frame; hook this for analytics or your own retry chrome. A coarse reason only — never the buyer's email, the raw backend error, or any PII.

| Field | Description |
|---|---|
| `planVersionId` | The plan version the attempt was for. |
| `planItemId` | The item the attempt was for. |
| `reason` | Coarse, non-sensitive classification: `SESSION_MINT_FAILED` (the checkout session could not be created — self-signup turned off mid-flow, or a transient 5xx), `CONFIRM_FAILED` (a confirm submit was rejected — expired or double-spent session, stale terms, invalid start date). |

## `ledgerbee:embed:checkoutClosed`

The buyer closed a forced single-item checkout (Cancel/Back on a `…/embed/checkout/<vanity>/<planItemId>` or gated `…/by-id/…` surface, which has no cards to fall back to, so it lands on a terminal "Checkout closed" state). The pricing-card surface instead returns the buyer to its cards and emits nothing. A failed session mint emits `checkoutError`, not this — you never see both for one attempt.

| Field | Description |
|---|---|
| `planVersionId` | The plan version that was abandoned. |
| `planItemId` | The item that was abandoned. |

{/* @codegen-end embed-lifecycle-events */}

## When no event fires

`checkoutError` is session-mint/confirm-specific — it covers *in-frame* checkout
attempts only. If the browser refuses to frame the embed at all (CSP
`frame-ancestors` — your origin isn't on the partner-origin allowlist), **no
embed event fires** and `pageLoaded` never arrives. Detect it by the absence of
`pageLoaded` plus the console error, not by listening for an error event. See
[Errors](/guides/embedded-checkout/fulfillment#errors--failure-states).
