Integration Options
Ways to open the hosted checkout from your site, and how to hand the integration to a coding agent.
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) and storesorder.id. - Customer is sent to
pay.peer.xyz/?order=...&token=.... - Customer completes payment.
- Checkout shows a Return to merchant link built from your
successUrl, which the customer clicks to come back. Nothing redirects on its own, andcancelUrlis never navigated to. See whatsuccessUrlandcancelUrldo.
Never fulfill an order on that return. Fulfill on the ORDER_FULFILLED webhook.
URL helpers
Three helpers build or follow a checkout URL. getCheckoutUrl and redirectToCheckout
never read apiKey, so leave it out of their options.
getCheckoutUrl
Builds a checkout URL without navigating.
function getCheckoutUrl(
orderId: string,
orderToken: string,
opts: CheckoutClientOptions
): string
import { createCheckout, getCheckoutUrl } from '@zkp2p/pay-sdk';
const checkout = await createCheckout(params, opts);
const url = getCheckoutUrl(checkout.order.id, checkout.orderToken, {
apiBaseUrl: 'https://api.pay.peer.xyz',
checkoutBaseUrl: 'https://pay.peer.xyz',
});
console.log(url);
// https://pay.peer.xyz/?order=cmf5k2x9d0001abcd1234efgh&token=...
createCheckout already returns this URL as checkoutUrl. Reach for getCheckoutUrl when
you need to rebuild the URL later from a stored order id and token.
redirectToCheckout
Navigates the current browser tab to checkout.
function redirectToCheckout(
orderId: string,
orderToken: string,
opts: CheckoutClientOptions
): void
import { redirectToCheckout } from '@zkp2p/pay-sdk';
redirectToCheckout(orderId, orderToken, {
apiBaseUrl: 'https://api.pay.peer.xyz',
checkoutBaseUrl: 'https://pay.peer.xyz',
});
It calls window.location.assign, so it requires a DOM and only runs in browser code.
createCheckoutAndRedirect
Creates an order and redirects in one call.
function createCheckoutAndRedirect(
params: CreateOrderRequest,
opts: CheckoutClientOptions
): Promise<CreateCheckoutResult>
createCheckoutAndRedirect redirects, so it runs only in a browser, and createCheckout
requires apiKey. Using it puts your merchant API key in client code. Create the order on
your backend and send the customer the returned checkoutUrl instead.
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. The
Return to merchant link is not shown in embedded mode, so drive the parent page from
the checkout.success event below and from your webhooks.
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 the sandbox attribute entirely
also works; the requirement applies only once you sandbox the iframe.
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 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. Checkout then shows a Go Back
button that emits checkout.closed:
{
"channel": "zkp2p_checkout_embed_v1",
"type": "checkout.closed",
"timestamp": 1725000000000,
"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 with the Orders API or from your webhook stream.
Let a coding agent integrate
Use the Peer Pay CLI to sign in, configure your merchant, and hand the integration to your coding agent from your codebase:
peer-pay-cli login --profile live
peer-pay-cli setup
peer-pay-cli handoff --profile live --project .
The agent receives your CLI profile reference without embedded credentials. Use
peer-pay-cli integration runbook --profile live to read the public runbook.
The runbook covers order creation, the redirect, a raw-body webhook handler, webhook registration per environment, sandbox verification, and go-live. Two endpoints support that flow, and you can call them yourself:
| Endpoint | Auth | Does |
|---|---|---|
POST /api/v1/sandbox/test-order | Sandbox key | Creates, pays, and settles a sandbox order in one call and fires a genuine signed ORDER_FULFILLED delivery at your registered sandbox webhook. No money moves. Pass orderId to settle an open sandbox order your storefront created instead; optional requestedUsdcAmount (default 1.00) and rail. Limited to 5 calls per 10 minutes per merchant. |
GET /api/v1/integration/status | Either key | Onboarding steps with per-step evidence (sandbox and live webhook registrations, last delivery response code, whether live orders can be created), plus verified, liveWebhookReady, nextAction, and nextAgentAction. |
Read verified rather than complete. verified becomes true only once your handler has accepted a signed sandbox delivery, while complete tracks the onboarding checklist and becomes true with the first live order. Steps tagged "actor": "MERCHANT" need a dashboard session, so the agent reports them back to you. Picking a plan under Settings, Billing is the usual one.
Recommended Backend Pattern
const opts = {
apiBaseUrl: 'https://api.pay.peer.xyz',
checkoutBaseUrl: 'https://pay.peer.xyz',
apiKey: process.env.ZKPAY_API_KEY!,
};
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. Do not cancel an order solely
because PAYMENT_EXPIRED arrived; see PAYMENT_EXPIRED.
Write the handler itself against
webhook verification, which has the raw-body signature check in
Node, Python, and Go.
Onramp Into User Wallet
Settle straight to the customer's own wallet by overriding destinationAddress per order.
const checkout = await createCheckout(
{
requestedUsdcAmount: '100.00',
destinationChainId: 8453,
destinationToken: 'USDC',
destinationAddress: userWalletAddress,
successUrl: `${process.env.APP_URL}/onramp/success`,
notes: {
type: 'onramp',
userWallet: userWalletAddress,
},
},
opts,
);