Developer documentation
OPB API v1
Accept JazzCash, EasyPaisa and Bank QR with one REST API. Create a payment on your server, redirect the customer to the hosted checkout, and receive a signed webhook when it completes.
Base URL https://zaod-opb.lovable.app/api/public/v1
Format JSON over HTTPS
Currency PKRAuthentication
Send your secret key as a Bearer token. sk_test_… keys create sandbox payments; sk_live_… keys create real payments. Find both in Dashboard → Developer. Secret keys must only live on your server.
Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxCreate a payment
POST /payments
| Field | Type | Description |
|---|---|---|
| amount | number | Required. PKR, up to 2 decimals |
| order_id | string | Required. Your order reference (≤100) |
| description | string | Optional. Shown on checkout |
| customer_name | string | Optional |
| customer_phone | string | Optional |
| return_url | url | Optional. Customer is redirected here with ?payment_id&order_id&status. Falls back to the dashboard default. |
const res = await fetch("https://zaod-opb.lovable.app/api/public/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPB_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 2500,
order_id: "ORDER-1001",
description: "Blue T-shirt",
return_url: "https://yourshop.pk/thank-you",
}),
});
const payment = await res.json();
res.redirect(payment.checkout_url); // ExpressResponse · 201
{
"id": "pay_3f9c1a2b4d5e6f708192a3b4",
"mode": "sandbox",
"amount": 2500,
"currency": "PKR",
"order_id": "ORDER-1001",
"status": "created",
"reference": "OPB8A1C2D3E4F",
"channel": null,
"created_at": "2026-10-08T10:00:00Z",
"paid_at": null,
"expires_at": "2026-10-08T10:15:00Z",
"checkout_url": "https://zaod-opb.lovable.app/checkout/pay_3f9c1a2b4d5e6f708192a3b4"
}Check payment status
GET /payments/:id
Statuses: created · pending · success · failed · expired. Always confirm with this endpoint or a verified webhook before shipping an order — never trust the return URL alone.
const r = await fetch(`https://zaod-opb.lovable.app/api/public/v1/payments/${paymentId}`, {
headers: { Authorization: `Bearer ${process.env.OPB_SECRET_KEY}` },
});
const { status } = await r.json();| HTTP | Error |
|---|---|
| 401 | invalid_api_key |
| 404 | not_found |
| 422 | validation_error |
| 400 | invalid_json |
Hosted checkout
Redirect the customer to checkout_url. They pick JazzCash (Mobile Account, Card or Voucher), EasyPaisa or Bank QR, follow instructions with a 15-minute timer, and are sent back to your return URL after confirmation.
Webhooks
OPB sends a POST to your webhook URL for payment.successful, payment.failed and payment.pending. Respond with 2xx within 8 seconds.
POST /opb/webhook
Content-Type: application/json
X-OPB-Event: payment.successful
X-OPB-Signature: t=1791453600,v1=5f2b…c9e1
{
"id": "evt_9b1d…",
"event": "payment.successful",
"created_at": "2026-10-08T10:03:12Z",
"data": {
"id": "pay_3f9c1a2b4d5e6f708192a3b4",
"order_id": "ORDER-1001",
"reference": "OPB8A1C2D3E4F",
"amount": 2500,
"currency": "PKR",
"status": "success",
"channel": "jazzcash",
"method": "mwallet",
"mode": "sandbox"
}
}{
"event": "payment.failed",
"data": {
"id": "pay_…",
"order_id": "ORDER-1001",
"status": "failed",
"channel": "easypaisa"
}
}Verify signatures (HMAC SHA-256)
- Read the raw request body (before JSON parsing).
- Split X-OPB-Signature into t and v1.
- Compute HMAC_SHA256(secret_key, t + "." + raw_body) as hex.
- Compare with v1 in constant time and reject timestamps older than 5 minutes.
Sandbox events are signed with your sk_test_ key; live events with sk_live_.
import crypto from "node:crypto";
// Express: app.post("/opb/webhook", express.raw({ type: "application/json" }), handler)
function handler(req, res) {
const raw = req.body.toString("utf8");
const parts = Object.fromEntries(req.get("x-opb-signature").split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", process.env.OPB_SECRET_KEY)
.update(`${parts.t}.${raw}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
if (!ok || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return res.sendStatus(401);
const event = JSON.parse(raw);
if (event.event === "payment.successful") { /* mark order paid */ }
res.sendStatus(200);
}Sandbox
Use sandbox keys to build without moving money. Sandbox checkouts show "Simulate success" and "Simulate failure" buttons, fire real signed webhooks, and never touch your wallet balance. Use the Webhook Tester in Dashboard → Developer to send sample events and inspect your endpoint's response.