> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cryptocheckout.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Browser events

> The callbacks the embedded checkout fires, and the one rule for using them safely.

The checkout runs in an iframe and talks to your page over `postMessage`. The SDK turns that into callbacks.

<Danger>
  **Never fulfil an order from a browser callback.**

  These fire in the customer's browser. If they close the tab, lose signal, or their battery dies, the callback never runs — but the payment still completed. Use [webhooks](/webhooks/overview) for fulfilment and these for UI.
</Danger>

## The callbacks

<ResponseField name="onReady" type="function">
  The checkout has loaded and completed on-chain verification. Good moment to hide your own loading state.
</ResponseField>

<ResponseField name="onPaymentConfirmed" type="function">
  The payment reached finality. Receives the payment payload.

  Use it to redirect to a thank-you page or show a success state — not to release goods.
</ResponseField>

<ResponseField name="onPaymentFailed" type="function">
  The payment attempt failed. Receives an error payload.
</ResponseField>

<ResponseField name="onWidgetClosed" type="function">
  The customer dismissed the checkout. Fires whether or not they paid — a customer can pay and then close, so don't read this as abandonment on its own.
</ResponseField>

## Typical usage

```javascript theme={null}
const checkout = CryptoCheckout.init({
  merchantId: "…",
  settlementAnchor: "0x…",

  onReady: () => {
    setLoading(false);
  },

  onPaymentConfirmed: (payload) => {
    // UI only. Your webhook is what actually fulfils.
    analytics.track("payment_confirmed", { orderId: payload?.orderId });
    window.location.href = "/thank-you";
  },

  onPaymentFailed: (err) => {
    showBanner("Payment didn't complete. Nothing was charged.");
    console.error(err);
  },

  onWidgetClosed: () => {
    // Don't assume abandonment — the webhook may still arrive.
    setLoading(false);
  },
});
```

## The race you need to handle

A customer can pay and immediately close the tab. Your webhook fires; your `onPaymentConfirmed` doesn't.

```mermaid theme={null}
sequenceDiagram
    participant C as Customer
    participant S as Your server
    C->>C: Pays, closes tab
    Note over C: onPaymentConfirmed never fires
    S->>S: Webhook arrives
    S->>S: Order fulfilled
    Note over C,S: Customer returns later<br/>and finds the order complete
```

Design your thank-you page to read **order state from your server**, not from the callback. Then a customer who closes early and comes back still sees the right thing.

## Checkout states

The customer may see these. Worth recognising them in support conversations.

| State                  | What happened                                                                                |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| Verified               | On-chain verification passed. Normal.                                                        |
| Unverified badge       | No `settlementAnchor` pinned. Payment still works, verification didn't run.                  |
| Configuration mismatch | Verification **failed**. Signing blocked, address hidden. Check your payout settings.        |
| Recipient changed      | The on-chain payout address moved between quote and signature. Customer is asked to refresh. |
| Rate limited           | Too many requests. Counts down and retries automatically.                                    |
| Session expired        | Quote lapsed before payment. A fresh quote is issued.                                        |

<Card title="Set up webhooks" icon="webhook" href="/webhooks/overview" horizontal>
  The reliable path. Do this before going live.
</Card>
