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

# Overlay checkout (onflay.js)

> Open Onflay hosted checkout in a secure iframe overlay without redirecting buyers away from your app.

# Overlay checkout (`onflay.js`)

Use the CDN loader when you want buyers to pay without leaving your site.
Your **server** still creates the checkout session with a secret key — the
browser only opens the returned `embedUrl`.

<Warning>
  The overlay `onComplete` callback is a **UI signal**, not proof of payment.
  Always confirm with
  [`GET /v1/checkout-sessions/{id}/receipt?headless=true`](/developers/quickstart)
  using your `sk_*` key (or rely on webhooks).
</Warning>

## 1. Create an embedded session (server)

```ts theme={"dark"}
// app/api/checkout/route.ts
const response = await fetch("https://sandbox-api.onflay.com/v1/checkout-sessions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ONFLAY_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `checkout:${userId}:${listingId}`,
  },
  body: JSON.stringify({
    listingId,
    email: customerEmail,
    successUrl: "https://your-app.com/thanks",
    cancelUrl: "https://your-app.com/pricing",
    uiMode: "embedded",
  }),
});

const session = await response.json();
// session.embedUrl → pass to the browser
return Response.json({ embedUrl: session.embedUrl, sessionId: session.sessionId });
```

`uiMode: "embedded"` derives `frame-ancestors` from the origins of
`successUrl` / `cancelUrl`. Those pages must be the sites that host the overlay.

## 2. Load the script

```html theme={"dark"}
<script
  src="https://js.onflay.com/v1/onflay.js"
  integrity="sha384-3Qj5RXaH1JnzdX+n/Qg9HpsuBVunMwjTCajk1pGsKfPbQG0MCHvg6AqAhPxkhlyJ"
  crossorigin="anonymous"
></script>
<!-- or jsDelivr mirror -->
<script
  src="https://cdn.jsdelivr.net/npm/@onflay/checkout-js@0.1.0/dist/onflay.iife.js"
  integrity="sha384-3Qj5RXaH1JnzdX+n/Qg9HpsuBVunMwjTCajk1pGsKfPbQG0MCHvg6AqAhPxkhlyJ"
  crossorigin="anonymous"
></script>
```

Recompute the SRI hash after each release:

```bash theme={"dark"}
pnpm --filter @onflay/checkout-js build
openssl dgst -sha384 -binary sdks/browser/dist/onflay.iife.js | openssl base64 -A
```

npm:

```bash theme={"dark"}
pnpm add @onflay/checkout-js
```

```ts theme={"dark"}
import { OnflayCheckout } from "@onflay/checkout-js";
```

## 3. Open the overlay

```js theme={"dark"}
const { embedUrl } = await fetch("/api/checkout", { method: "POST" }).then((r) =>
  r.json(),
);

OnflayCheckout.open({
  url: embedUrl,
  onComplete: async ({ checkoutId }) => {
    await fetch("/api/confirm-checkout", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ checkoutId }),
    });
  },
  onCancel: () => {},
  onError: (error) => console.error(error),
  // If the iframe is blocked, redirect to hosted checkout (default).
  fallback: "redirect", // or "popup" | "none"
});
```

### Declarative markup

```html theme={"dark"}
<a
  href="https://checkout.onflay.com/s/FALLBACK_SESSION"
  data-onflay-checkout="EMBED_URL_FROM_SERVER"
>
  Buy now
</a>
```

## Appointment listings (pick time in the overlay)

For `APPOINTMENT` products you can omit `appointmentSlot` / `appointmentSlots`
and let the buyer pick time(s) inside the overlay before paying (same Flow B as
hosted checkout — see [Appointment checkout](/developers/appointment-checkout)).
Session packs collect all N times in the overlay UI:

```ts theme={"dark"}
body: JSON.stringify({
  listingId,
  variantId,
  email: customerEmail,
  successUrl: "https://your-app.com/booking/success",
  cancelUrl: "https://your-app.com/booking",
  uiMode: "embedded",
  // no appointmentSlot / appointmentSlots → calendar appears in the iframe
}),
```

## Merchant CSP

Allow the loader script and the checkout iframe:

```
script-src https://js.onflay.com https://cdn.jsdelivr.net;
frame-src https://checkout.onflay.com https://sandbox-checkout.onflay.com https://js.stripe.com;
```

## Security model

| Control           | Behavior                                                            |
| ----------------- | ------------------------------------------------------------------- |
| Secret keys       | Stay on your server only                                            |
| Framing allowlist | Per-session `frame-ancestors` from success/cancel origins           |
| Loader URL check  | Only Onflay checkout hosts may be framed                            |
| postMessage       | Versioned `onflay.checkout.v1`, exact `targetOrigin`, source checks |
| Completion        | `checkoutId` only — verify with the receipt API                     |

## Fallback policy

If the iframe never posts `ready` (CSP mismatch, corporate proxy, ad blocker):

* `redirect` (default) — navigate to the embed/hosted URL top-level
* `popup` — open a checkout popup window
* `none` — call `onError` only
