Plug in, or build on it.
Install the WooCommerce plugin and you are done. If you run your own stack, create a checkout session with one API call, send the buyer to the hosted page, and let signed webhooks tell your system what happened.
curl https://api.zonipay.com/v1/checkout_sessions \
-H "Authorization: Bearer zp_test_..." \
-H "Content-Type: application/json" \
-d '{
"line_items": [
{ "name": "Reference peptide, 5 mg vial",
"unit_amount": 4800, "quantity": 2 }
],
"customer_email": "lab@example.edu",
"success_url": "https://yourstore.com/thanks",
"cancel_url": "https://yourstore.com/cart",
"metadata": { "order_id": "1042" }
}'
# {"id": "cs_test_8Hq2...", "url": "https://checkout.yourstore.com/c/cs_test_8Hq2..."}
Four ways in. Same checkout.
Whichever you choose, the buyer lands on the same hosted checkout on your subdomain, with the research-use attestation and the receipt from Zonipay.
WooCommerce plugin
Adds Zonipay as the payment method, sends the cart to the hosted checkout and updates the order status when payment lands.
Hosted checkout link
A payment link created in your dashboard for a fixed basket, to send by email or put behind a button.
REST API and Node.js SDK
Create a checkout session from your server with the cart you already hold, then redirect the buyer to its URL.
Signed webhooks
Events for paid orders, refunds, disputes, payouts and compliance findings, posted to your endpoint and signed.
One call, one URL.
Send the line items, the buyer's email and where to return them. You get back a session id and the url of the hosted page. Amounts are integers in the smallest unit of your account currency.
- Base URL
https://api.zonipay.com, versioned in the path - Authenticate with your secret key; never ship it to a browser
- Put your own order reference in
metadata: it comes back on every event - Treat the redirect to
success_urlas a courtesy, and theorder.paidwebhook as the record
The API is still being designed. Examples on this page show its intended shape; we confirm the exact surface with you during onboarding.
import Zonipay from "@zonipay/node";
const zonipay = new Zonipay(process.env.ZONIPAY_SECRET_KEY);
const session = await zonipay.checkoutSessions.create({
line_items: [
{ name: "Reference peptide, 5 mg vial", unit_amount: 4800, quantity: 2 },
],
customer_email: "lab@example.edu",
success_url: "https://yourstore.com/thanks",
cancel_url: "https://yourstore.com/cart",
metadata: { order_id: "1042" },
});
// Send the buyer to the hosted checkout
res.redirect(303, session.url);
const r = await fetch("https://api.zonipay.com/v1/checkout_sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ZONIPAY_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
line_items: [{ name: "Reference peptide, 5 mg vial", unit_amount: 4800, quantity: 2 }],
customer_email: "lab@example.edu",
success_url: "https://yourstore.com/thanks",
cancel_url: "https://yourstore.com/cart",
metadata: { order_id: "1042" },
}),
});
const { id, url } = await r.json();
import crypto from "node:crypto";
// header: "t=1767225600,v1=5f2b...", rawBody: the exact bytes received
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);
}
Check the signature before you trust it.
Every event is posted to your HTTPS endpoint with a Zonipay-Signature header. Verify it with the signing secret shown for that endpoint, then act.
Six events cover the lifecycle.
| Event | Sent when | Typical action |
|---|---|---|
order.paid | A checkout session is paid and the attestation is recorded. | Mark the order paid, start fulfilment. |
order.refunded | All or part of an order is refunded to the buyer. | Update the order and stock. |
dispute.opened | A buyer's bank opens a chargeback on an order. | Gather tracking and proof of delivery for our request. |
dispute.closed | The dispute is decided, won or lost. | Record the outcome on the order. |
payout.paid | A payout is sent to your bank account. | Reconcile it against the orders it lists. |
compliance.finding | A storefront scan raises a finding, with severity, page and change needed. | Route it to whoever edits the store. |
Sandbox and test mode
Your account comes with a sandbox. Test keys start with zp_test_ and live keys with zp_live_; nothing made with a test key moves money. Use the test cards listed in your sandbox to try successful payments, declines and 3-D Secure challenges, and send test events to your webhook endpoint.
zp_test_...zp_live_...Versioning
The version is in the path: /v1. Within a version we only make additive changes, such as new optional fields or new event types, so your code should ignore fields it does not know. A change that could break an integration ships as a new version, announced by email in advance, and the previous version keeps working while you move.
The full walkthrough.
Keys, sessions, webhooks, fulfilment and the go-live checklist, in order.