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

# Embedded Checkout

> Render the Soap checkout inside your own page with an iframe, instead of redirecting away

## Redirect vs. embed

After you [create a checkout session](/api-reference/api-v1/checkouts/create), the response includes a hosted `url`. You can present it two ways:

* **Redirect** — send the customer to `url` (`window.location.href = url`). Simplest to integrate; the customer leaves your page and comes back via `return_url`. See the [Quickstart](/get_started/quickstart).
* **Embed** — load `url` in an `<iframe>` so checkout renders in context, without a full-page navigation. This page covers embedding.

Both use the exact same server-side session creation — embedding changes only how the returned `url` is presented on your frontend.

<Note>
  Soap checkout is **cookieless**: the session's `client_secret` travels as a query parameter on every request, so cross-origin embedding does **not** depend on third-party cookies and needs no special browser configuration.
</Note>

## Before you start: register your embedding domain

Two things are tied to the domain of the page that hosts the iframe (your site's top-level domain). Share your production and staging origins with Soap once, during onboarding:

1. **Framing allowlist** — Soap's checkout only allows itself to be framed by approved origins (CSP `frame-ancestors`). Until your origin is allowlisted, the iframe will render blank.
2. **Apple Pay domain registration** — Apple validates Apple Pay against the top-level page, so your domain must be registered for Apple Pay web merchant validation. Cards and Google Pay work without this.

## 1. Create the session on your backend

Almost unchanged from the redirect flow — create the checkout server-side (so your API key stays private), pass a `return_url`, and set **`experience: "iframe"`**. That flag tells Soap the checkout will be framed: Soap redirects the customer to `return_url` **inside the iframe** on completion, and Apple Pay validates against your embedding domain instead of Soap's hosted one.

```javascript theme={null}
app.post('/checkout-session', async (req, res) => {
  const response = await fetch('https://api-sandbox.paywithsoap.com/api/v1/checkouts', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SOAP_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      customer_id: req.body.customer_id,
      type: 'deposit',
      experience: 'iframe',
      return_url: 'https://your-site.com/checkout-complete'
    })
  });

  const checkout = await response.json();
  res.json({ checkoutUrl: checkout.url });
});
```

<Warning>
  Without `experience: "iframe"` the checkout still renders inside your frame, but Apple Pay will fail: sessions are validated against Soap's hosted domain, which won't match your top-level page.
</Warning>

## 2. Embed the checkout URL in an iframe

Load the returned `url` into an `<iframe>`. The **`allow="payment"`** attribute is required for Apple Pay, Google Pay, and other Payment Request wallets to work inside the frame.

```html theme={null}
<iframe
  title="Checkout"
  allow="camera; microphone; geolocation; payment; clipboard-write"
  style="width: 100%; height: 720px; border: 0;">
</iframe>

<script>
  async function openCheckout() {
    const res = await fetch('/checkout-session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ customer_id: 'cus_...' })
    });
    const { checkoutUrl } = await res.json();
    document.querySelector('iframe').src = checkoutUrl;
  }
</script>
```

<Warning>
  Without `allow="payment"` the checkout still renders and cards work, but Apple Pay / Google Pay buttons will not appear or will fail to open. The other permissions (`camera`, `microphone`, `geolocation`, `clipboard-write`) support identity verification, geo-checks, and copy actions used by some flows.
</Warning>

## 3. Know when checkout completes

Use **two signals**, in order of authority:

### Webhooks (source of truth)

Payment state is confirmed server-side. Always reconcile off [webhooks](/api-reference/api-v1/webhooks) — never treat a client-side event as proof of payment.

### `return_url` + `postMessage` (UX hint)

On completion the checkout redirects **within the iframe** to your `return_url`. Because that page loads inside the frame, it can notify your top-level page with `postMessage`:

```html theme={null}
<!-- your-site.com/checkout-complete -->
<script>
  if (window.parent !== window) {
    window.parent.postMessage({ type: 'soap:checkout-complete' }, 'https://your-site.com');
  }
</script>
```

Listen for it on the embedding page to update your UI (close the frame, show a receipt, etc.):

```javascript theme={null}
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://your-site.com') return;
  if (event.data?.type === 'soap:checkout-complete') {
    // update UI — then confirm the payment via your webhook / backend
  }
});
```

<Note>
  The completion message is sent by **your own `return_url` page** (it loads inside the iframe after the checkout redirects to it), so `event.origin` is *your* site's origin — not the Soap wallet domain. Validate it against your own origin as shown above, and treat the message as a UX hint only: confirm the payment via webhooks.
</Note>

## Caveats

<AccordionGroup>
  <Accordion title="Apple Pay requires registering your embedding domain">
    Apple validates Apple Pay against the **top-level page domain** — the site hosting the iframe, not the checkout iframe itself. To offer Apple Pay in-frame you need both pieces from this guide: your embedding domain registered with Soap for Apple Pay web merchant validation (see [Before you start](#before-you-start-register-your-embedding-domain)), and the session created with `experience: "iframe"`. The checkout detects your top-level domain automatically at payment time — no extra frontend work.
  </Accordion>

  <Accordion title="Bank login (Plaid / Link Money) may open a new window">
    Banks refuse to be framed, so OAuth-style bank connection can open in a popout/new window and return control to the iframe afterward. Every other method — cards, Apple Pay, Google Pay — stays fully in-frame.
  </Accordion>

  <Accordion title="Sandbox vs. production URLs">
    Sandbox checkout URLs are on `wallet-sandbox.paywithsoap.com`; production on `wallet.paywithsoap.com`. The `url` in the API response already points at the right one for the key you used.
  </Accordion>
</AccordionGroup>
