Skip to main content

Redirect vs. embed

After you create a checkout session, 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.
  • 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.
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.

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.
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 });
});
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.

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.
<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>
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.

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 — 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:
<!-- 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.):
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
  }
});
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.

Caveats

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), and the session created with experience: "iframe". The checkout detects your top-level domain automatically at payment time — no extra frontend work.
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.