# Embedded checkout

> Take crypto payments on your own website without redirecting. Open the LayerHop checkout as a popup or inline with checkout.js, @layerhop/widget or a React component, and react to paid, cancel and status events.

Source: https://docs.layerhop.com/guides/embedded-checkout

The embedded checkout shows the LayerHop payment page **on your website**, in a popup or inline, so customers pay without leaving your site. It is the same checkout as the hosted page (any token, any chain, delivery verified on chain), loaded in a secure iframe from layerhop.com. Wallets connect inside it; your page never touches keys or funds.

You need either a **checkout session** created by your server ([Checkout](/guides/checkout)) or a **product** id ([Payment buttons](/guides/payment-buttons)).

## Script tag

```html
<script src="https://layerhop.com/checkout.js" async></script>
<script>
  async function pay() {
    // Your server created the session with your API key and returned its id
    const { sessionId } = await fetch("/api/create-checkout", { method: "POST" }).then((r) => r.json());
    LayerHopCheckout.open({
      sessionId,
      onPaid: (session) => (location.href = "/thanks?session=" + session.id),
      onCancel: () => console.log("closed without paying"),
    });
  }
</script>
<button onclick="pay()">Pay with crypto</button>
```

Or with a product, no server at all:

```js
LayerHopCheckout.open({ productId: "prod_…", reference: "order_1042", onPaid: (session) => { … } });
```

Show the checkout inside your page instead of a popup:

```html
<div id="pay"></div>
<script>
  LayerHopCheckout.mount("#pay", { sessionId: "cs_…", onPaid: (session) => { … } });
</script>
```

The inline frame resizes to fit its content.

## npm

```bash
npm install @layerhop/widget
```

```ts
import { openCheckout, mountCheckout } from "@layerhop/widget";

const checkout = openCheckout({ sessionId, onPaid: (session) => showThanks(session) });
// checkout.close() to dismiss it from your code
```

React:

```tsx
import { LayerHopCheckout } from "@layerhop/widget/react";

export function Pay({ sessionId }: { sessionId: string }) {
  return <LayerHopCheckout sessionId={sessionId} onPaid={(s) => router.push(`/thanks/${s.id}`)} />;
}
```

## Options and events

| Option | Meaning |
|---|---|
| `sessionId` | A session your server created (`POST /v1/checkout/sessions`). |
| `productId` | A product: the library starts a session for it (`reference` and `metadata` optional). |
| `onReady()` | The checkout has loaded. |
| `onStatus(status)` | The session status changed (`processing`, `paid`, …). |
| `onPaid(session)` | LayerHop verified the payment. The session is `paid`. In the browser it is the public view (no `reference` or `metadata`); look those up on your server by `session.id`, or read them from the webhook. |
| `onCancel()` | The customer cancelled. |
| `onClose()` | The popup closed (after paying or not). |

The iframe talks to your page with `postMessage` messages `{ source: "layerhop-checkout", version: 1, type, sessionId, … }`, where `type` is `ready`, `resize`, `status`, `paid`, `cancel` or `close`. The library checks that each message comes from layerhop.com and from its own frame before calling your callbacks.

> [!IMPORTANT]
> Browser callbacks are for the user interface. Fulfil orders on the signed `checkout.session.completed` [webhook](/guides/webhooks), or by fetching the session from your server with your key, never because the browser said so.

## Wallets inside the frame

Browser-extension wallets (MetaMask, Phantom, Rabby, Coinbase Wallet and others) and WalletConnect QR codes work inside the embedded checkout. On phones, the popup becomes a full-screen sheet.

## Test it

Create the session or product with an `lh_test_` key: the checkout shows a **Test mode** badge and a **Simulate test payment** button, and `onPaid` fires about 10 to 20 seconds later. See [Test mode](/guides/test-mode).
