Home/Developers/Integration guide
Integration guide

From test key to first real order.

Seven steps for a custom integration with the REST API. On WooCommerce, the plugin covers steps 2 to 6: read step 1 and the go-live checklist.

APIv1Base URLapi.zonipay.comExamplesNode.js

The API is still being designed. Names and fields below show its intended shape, and we confirm the exact surface with you during onboarding. Questions at any step: support@zonipay.com.

01Get your keys

Your dashboard has two sets of keys. Build everything with the test key first; it runs against the sandbox and moves no money.

zp_test_...
Sandbox. Test cards only. Events go to your test webhook endpoint.
zp_live_...
Production. Available once your account has passed review.
Signing secret
One per webhook endpoint, shown when you add the endpoint. Used in step 5.

Keep secret keys on your server, in environment variables, out of source control and never in browser code. If a key leaks, roll it from the dashboard; the old one stops working.

.env
ZONIPAY_SECRET_KEY=zp_test_...
ZONIPAY_WEBHOOK_SECRET=...

02Create a checkout session

When the buyer clicks checkout on your store, your server creates a session with the cart. Amounts are integers in the smallest unit of your account currency, so 4800 is 48.00.

POST /v1/checkout_sessions
import Zonipay from "@zonipay/node";
const zonipay = new Zonipay(process.env.ZONIPAY_SECRET_KEY);

const session = await zonipay.checkoutSessions.create({
  line_items: cart.items.map((i) => ({
    name: i.title,             // "Reference peptide, 5 mg vial"
    unit_amount: i.priceCents, // 4800
    quantity: i.qty,
  })),
  customer_email: cart.email,
  success_url: "https://yourstore.com/thanks?order=1042",
  cancel_url: "https://yourstore.com/cart",
  metadata: { order_id: "1042" },
});
// session = { id: "cs_test_8Hq2...", url: "https://checkout.yourstore.com/c/cs_test_8Hq2..." }
line_items
Name, unit amount and quantity for each product. Use the product names shown on your storefront.
customer_email
Prefills the checkout. The receipt from Zonipay goes to this address.
success_url
Where the buyer returns after paying.
cancel_url
Where the buyer returns if they leave the checkout.
metadata
Your own references. Returned on every related event.

Save the session id against your pending order. You will match it when the webhook arrives.

03Redirect the buyer

Send the buyer to session.url. The hosted checkout runs on your checkout subdomain with your branding, collects the card or wallet payment, runs 3-D Secure when the issuer asks for it, and requires the research-use attestation before the pay button works.

Express
app.post("/checkout", async (req, res) => {
  const session = await createSessionFor(req.cart); // step 2
  res.redirect(303, session.url);
});
Your storeCart and checkout button
Hosted checkoutcheckout.yourstore.com
PaymentAttestation, card or wallet
success_urlBack on your store

Do not mark the order paid when the buyer reaches success_url. A buyer can close the tab before returning, and anyone can type the URL. The webhook in step 4 is the record.

04Handle webhooks

Add an HTTPS endpoint in your dashboard and choose the events it receives. Each event is a JSON body with an id, a type and a data object.

Example body: order.paid
{
  "id": "evt_3kP9...",
  "type": "order.paid",
  "created": 1767225600,
  "data": {
    "checkout_session": "cs_test_8Hq2...",
    "amount_total": 9600,
    "metadata": { "order_id": "1042" }
  }
}
Express webhook route
import express from "express";
import { verifySignature } from "./verify.js"; // step 5

// Raw body: the signature covers the exact bytes we sent
app.post("/webhooks/zonipay", express.raw({ type: "application/json" }), async (req, res) => {
  const ok = verifySignature(
    req.body.toString("utf8"),
    req.get("Zonipay-Signature") || "",
    process.env.ZONIPAY_WEBHOOK_SECRET
  );
  if (!ok) return res.sendStatus(400);

  const event = JSON.parse(req.body);
  if (await alreadyHandled(event.id)) return res.sendStatus(200);

  switch (event.type) {
    case "order.paid":        await markPaid(event.data.metadata.order_id); break;
    case "order.refunded":    await markRefunded(event.data); break;
    case "dispute.opened":    await flagForEvidence(event.data); break;
    case "compliance.finding": await notifyStoreEditor(event.data); break;
  }
  res.sendStatus(200);
});

Reply with a 2xx as soon as the event is stored, and do slow work afterwards. If your endpoint fails or times out, the event is sent again, so the same event can arrive more than once: record each id you have handled and skip repeats. The six event types are listed on the developers page.

05Verify the signature

Every request carries a header in this form:

Zonipay-Signature: t=1767225600,v1=5f2b9c...

v1 is the hex HMAC-SHA256, keyed with your endpoint's signing secret, of the string made of t, a full stop, and the raw body. Recompute it, compare in constant time, and reject timestamps outside a few minutes to stop replays.

verify.js
import crypto from "node:crypto";

export function verifySignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("="))
  );
  const t = Number(parts.t);
  if (!t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Common mistake: verifying against a body your framework has already parsed and re-serialised. Spacing and key order change, and the signature never matches. Always verify the raw bytes.

06Fulfil and add tracking

Ship only after order.paid. Then add the carrier and tracking number to the order in your dashboard; on WooCommerce, the plugin can pass them on when you mark the order shipped.

Tracking matters beyond the buyer's convenience. If a buyer disputes a charge, we answer the chargeback as merchant of record and ask you for tracking and proof of delivery, to be sent within 5 business days of our request. The research-use attestation record is attached by us. Orders with tracking already on file are the easiest to defend.

Refunds you issue from the dashboard trigger order.refunded, so your system stays in step. Our buyer refund standard is in the refund policy.

07Go-live checklist

Work through this list before switching to your live key. The boxes are for your own tracking and are not saved.

Then open the store to your customers. Storefront scans continue every 8 hours; see compliance for what they check. If anything in this guide does not match what you see, write to support@zonipay.com.