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.

POST/api/v1/payment-sessions
curl -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

javascript
import { 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 since 1.2.0 — pass mobileApp: { policy: 'off' } for web-only checkout.
  • Never put clientToken in deep links or QR payloads — sessionId only.
  • Handle onSessionUpdate for 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):

GET/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:

GET/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 / failureUrl pointing to an origin that is not on the allowlist.
  • Fulfilling on redirect before payment_session.completed is 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.