Integration Options
This guide covers common checkout integration patterns.
Full-Page Redirect (Recommended)
import { createCheckout } from '@zkp2p/pay-sdk';
const checkout = await createCheckout(params, opts);
window.location.href = checkout.checkoutUrl;
Flow
- Backend creates an order (
createCheckout). - Customer is redirected to
pay.peer.xyz/?order=...&token=.... - Customer completes payment.
- Customer returns via your
successUrlorcancelUrl.
New Tab
window.open(checkout.checkoutUrl, '_blank');
Good when you want to keep the merchant page open.
Embedded Iframe
Embedded checkout is supported with the @zkp2p/pay-sdk/embedded helpers.
import { useEffect, useRef } from 'react';
import { EMBED_EVENT_CHANNEL, ensureEmbedModeUrl } from '@zkp2p/pay-sdk/embedded';
export function EmbeddedCheckout({ checkoutUrl }: { checkoutUrl: string }) {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
useEffect(() => {
const onMessage = (event: MessageEvent) => {
if (event.source !== iframeRef.current?.contentWindow) return;
if (!event.data || event.data.channel !== EMBED_EVENT_CHANNEL) return;
if (event.data.type === 'checkout.success') {
// parent success handling
}
if (event.data.type === 'checkout.failed') {
// parent failure handling
}
if (event.data.type === 'checkout.closed') {
closeIframe(); // customer dismissed checkout — not a payment failure
}
};
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
}, []);
return (
<iframe
ref={iframeRef}
title="Embedded Checkout"
src={ensureEmbedModeUrl(checkoutUrl)}
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox"
/>
);
}
Sandbox requirements
If you set a sandbox attribute on the iframe, it must include allow-popups and
allow-popups-to-escape-sandbox, as shown above. Omitting a sandbox attribute entirely
also works — the restriction only applies once you opt in to sandboxing.
Embedded checkout opens payment apps in a new tab rather than navigating the iframe.
Payment providers including Cash App and PayPal serve X-Frame-Options: SAMEORIGIN, so
loading them in-frame is refused by the browser. The two flags cover that handoff:
| Flag | Why it is needed |
|---|---|
allow-popups | Lets the Pay with … and Verify to complete order buttons open the payment app at all. Without it the browser silently blocks the click and nothing happens. |
allow-popups-to-escape-sandbox | Stops the opened tab inheriting the iframe's sandbox. Without it the payment provider's own page loads under your restrictions and can misbehave. |
allow-scripts and allow-same-origin are also required for checkout to run and to post
the events described above.
If customers report that the Pay with … button does nothing, check the sandbox
attribute before anything else. A blocked popup produces no visible error in the
checkout UI — the click is simply dropped, and the order later expires unpaid.
Dismissing the iframe
An embedded checkout can reach a state the customer cannot pay from — for example when
no payment method has liquidity for the order amount. Rather than leave a dead Start
Payment button, checkout shows a Go Back button that emits checkout.closed:
{
"channel": "zkp2p_checkout_embed_v1",
"type": "checkout.closed",
"payload": {
"order_id": "...",
"reason": "no_payment_methods"
}
}
checkout.closed is a dismissal, not a failure. Close the iframe without running your
checkout.failed handling; showing a payment-failure message here would be misleading.
The order can be reopened later with the same checkout URL.
checkout.closed does not guarantee nothing was charged. A partially paid order returns
to method selection to pay its remaining balance, and if no rail has liquidity for that
remainder it can emit checkout.closed too. Before telling a customer nothing was taken,
check the authoritative order state by order_id or from your webhook stream.
Recommended Backend Pattern
const checkout = await createCheckout(
{
requestedUsdcAmount: '50.00',
destinationChainId: 8453,
destinationToken: 'USDC',
successUrl: 'https://yoursite.com/payment/success',
cancelUrl: 'https://yoursite.com/payment/cancelled',
notes: { merchantOrderId: orderId },
},
opts,
);
await db.orders.update({
where: { id: orderId },
data: { checkoutOrderId: checkout.order.id },
});
return checkout.checkoutUrl;
Use webhooks for payment and order state updates, especially PAYMENT_SETTLED, PAYMENT_FAILED, PAYMENT_EXPIRED, and ORDER_FULFILLED. PAYMENT_EXPIRED means the 1-hour customer payment window elapsed; do not cancel the order solely from that event because late on-chain settlements can still fulfill it.
Checkout supports multi-currency quote selection and payment FX snapshots.