Skip to main content

Integration Options

Ways to open the hosted checkout from your site, and how to hand the integration to a coding agent.

import { createCheckout } from '@zkp2p/pay-sdk';

const checkout = await createCheckout(params, opts);
window.location.href = checkout.checkoutUrl;

Flow​

  1. Backend creates an order (createCheckout) and stores order.id.
  2. Customer is sent to pay.peer.xyz/?order=...&token=....
  3. Customer completes payment.
  4. Checkout shows a Return to merchant link built from your successUrl, which the customer clicks to come back. Nothing redirects on its own, and cancelUrl is never navigated to. See what successUrl and cancelUrl do.

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>
Not for the pattern these docs recommend

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:

FlagWhy it is needed
allow-popupsLets 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-sandboxStops 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.

Symptom of a missing flag

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.

Not proof that nothing was charged

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:

EndpointAuthDoes
POST /api/v1/sandbox/test-orderSandbox keyCreates, 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/statusEither keyOnboarding 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.

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,
);