Build
Merchant integration guide
Create sessions on your server, hand off to the SDK, poll or reconcile, and confirm with webhooks.
This guide walks through the recommended merchant integration: create a session on your server, hand off to the Checkout SDK in the browser, confirm with signed webhooks, and handle customer redirects safely.
Go-live prerequisites
Session create requires more than a valid API key. Complete these portal steps before your first POST /payment-sessions call:
- KYB approved
- Business status ACTIVE
- Business Zippy payment ID created
- API key created
- All production checkout domains registered (Integrations → Allowed browser origins)
- successUrl / failureUrl origins match registered domains (if used)
See the Dashboard guide for portal workflows and allowed origin registration rules.
Create a session
When the customer is ready to pay, your backend creates a payment session with the locked cart total. Authenticate with X-Api-Key and include Idempotency-Key plus X-Correlation-Id.
/api/v1/payment-sessionscurl -X POST "https://api.zippypay.io/api/v1/payment-sessions" \
-H "Content-Type: application/json" \
-H "X-Api-Key: zp_live_REDACTED" \
-H "Idempotency-Key: 83596f5e-2134-4e66-8304-20d61f290cf0" \
-H "X-Correlation-Id: merchant-request-123" \
-d '{
"amount": "25.00",
"currency": "USD",
"merchantReference": "order-12345",
"successUrl": "https://yourstore.com/checkout/success",
"failureUrl": "https://yourstore.com/checkout/failure",
"metadata": {
"orderId": "12345"
}
}'Response excerpt
json{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"clientToken": "zps_REDACTED",
"expiresAt": "2026-08-31T12:30:00.000Z"
}Hand off to the SDK
Pass sessionId and clientToken to the browser through your own secure channel (for example a server-rendered page or an authenticated API response). Initialize checkout:
Checkout SDK handoff
javascriptimport { ZippyPay } from "@zippypay/checkout";
// Values from POST /api/v1/payment-sessions (server-side only for the API key)
const checkout = ZippyPay.create({
sessionId: "550e8400-e29b-41d4-a716-446655440000",
clientToken: "zps_REDACTED",
container: "#zippy-checkout",
redirectOnComplete: true,
onComplete: (result) => {
// Optional: UI feedback before redirect to successUrl
console.log("Session completed", result.sessionId);
},
onError: (error) => {
console.error("Checkout error", error);
},
});- Never expose
zp_live_to client code. - Use SDK
1.2.1(recommended). On phone, Pay with Zippy is the default since1.2.0— passmobileApp: { policy: 'off' }for web-only checkout. - Never put
clientTokenin deep links or QR payloads —sessionIdonly. - Handle
onSessionUpdatefor amount and status; do not hard-code amounts in page markup. - Mount checkout only after session creation succeeds.
- Register the checkout page origin under Integrations → Allowed browser origins before embedding. Unregistered domains return
403 payment-session.origin-not-allowed. - Call
destroy()when the customer navigates away. See Configuration and lifecycle.
Poll or reconcile
Prefer webhooks as your primary confirmation path. From your backend, poll merchant GET endpoints until status is terminal (COMPLETED, EXPIRED, CANCELLED, or FAILED):
/api/v1/payment-sessions/{sessionId}curl "https://api.zippypay.io/api/v1/payment-sessions/550e8400-e29b-41d4-a716-446655440000" \
-H "X-Api-Key: zp_live_REDACTED" \
-H "X-Correlation-Id: merchant-request-123"When you only have your order reference, look up by merchantReference:
/api/v1/payment-sessions?merchantReference={ref}curl "https://api.zippypay.io/api/v1/payment-sessions?merchantReference=order-12345" \
-H "X-Api-Key: zp_live_REDACTED" \
-H "X-Correlation-Id: merchant-request-123"200 — completed (merchant-safe)
json{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"status": "COMPLETED",
"amount": "25.00",
"currency": "USD",
"merchantReference": "order-12345",
"metadata": { "orderId": "12345" },
"transactionId": "880e8400-e29b-41d4-a716-446655440003",
"transactionStatus": "COMPLETED",
"payer": {
"paymentId": "john.doe12",
"displayName": "John Smith"
}
}Confirm with webhooks
Fulfill orders when you receive a verified payment_session.completed webhook. Match paymentSessionId or merchantReference to your internal order record:
payment_session.completed
json{
"id": "11111111-1111-4111-8111-111111111111",
"type": "payment_session.completed",
"createdAt": "2026-08-31T12:00:00.000Z",
"data": {
"paymentSessionId": "550e8400-e29b-41d4-a716-446655440000",
"status": "COMPLETED",
"merchantReference": "order-12345",
"amount": "25.00",
"currency": "USD",
"transactionId": "880e8400-e29b-41d4-a716-446655440003",
"transactionStatus": "COMPLETED",
"payer": {
"paymentId": "john.doe12",
"displayName": "John S."
}
}
}Redirect handling
If redirectOnComplete is enabled, the SDK sends customers to the successUrl or failureUrl you provided at session creation. Both URLs must use HTTPS and their origin must match a registered allowed checkout domain or your registered business website. Use redirects for UX, not as your sole source of truth:
Success page pattern
javascript// successUrl example: https://yourstore.com/checkout/success?sessionId=...
// Parse sessionId from query params, then reconcile on your server:
app.get("/checkout/success", async (req, res) => {
const sessionId = req.query.sessionId;
// Do NOT fulfill here without webhook verification.
// Show a "processing" page until payment_session.completed arrives.
res.render("success-pending", { sessionId });
});Show a processing state on the success page until your webhook handler marks the order paid. Handle payment_session.expired and payment_session.payment_started for abandoned carts and in-progress payments.
Common failures
- Creating a session in the browser instead of on your server.
- Going live with KYB approved but business status not ACTIVE, or without a receive payment ID configured.
- Embedding checkout on a domain not registered under allowed browser origins.
successUrl/failureUrlpointing to an origin that is not on the allowlist.- Fulfilling on redirect before
payment_session.completedis verified. - Reusing the same checkout embed after a terminal session — create a new session instead.
- Mismatch between locked session amount and your cart total at fulfillment time.
- Missing idempotency on session create, leading to duplicate charges for retried requests.