Integrate QRPayBD with your website
This guide is for developers connecting a custom website or app. Your server creates an order, sends the customer to the QRPayBD payment page, and learns about the payment through a signed webhook.
How it works
Your server talks to QRPayBD in four steps:
- Your server creates an order with the amount and your own order number.
- You send the customer to the payment page (the paymentUrl in the response). It shows your shop's wallet number, the exact amount and an order reference.
- The customer sends money in their wallet app with the order reference. If the app has no Reference field, they enter the TrxID from their payment message on that page.
- QRPayBD matches the reference or TrxID and the amount with the payment SMS read by your Collector app, marks the order paid and sends your server a signed webhook.
QRPayBD never holds or moves money. It confirms payments that already reached your own wallet or bank account.
Before you start
- Create your account at https://qrpaybd.com/app/ and add at least one payment method with your wallet number (Store setup, then Payment methods).
- Install the Collector app on the phone that receives your payment SMS and pair it with your account.
- In Store setup create a store API key. It is shown once. Keep it as a secret on your server, never in a web page or a mobile app.
- In Store setup set your webhook address (a public https:// address) and copy the webhook secret it shows.
All examples use the address https://qrpaybd.com. Send the header Authorization: Bearer YOUR_STORE_API_KEY with every request, from your server only.
Step 1: create an order
Send POST /orders from your server. amount is the total in taka as a JSON number (decimals are allowed). storeOrderId is your own unique order number. returnUrl is optional: if you send it, the payment page shows a Back to shop button after the payment.
curl -X POST https://qrpaybd.com/orders \
-H "Authorization: Bearer YOUR_STORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 1250.50, "storeOrderId": "ORDER-1001", "returnUrl": "https://yourshop.com/thank-you"}'const res = await fetch("https://qrpaybd.com/orders", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QRPAYBD_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ amount: 1250.5, storeOrderId: "ORDER-1001" }),
});
if (!res.ok) throw new Error(`QRPayBD error ${res.status}`);
const order = await res.json();
// Redirect the customer's browser to order.paymentUrl$ch = curl_init('https://qrpaybd.com/orders');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('QRPAYBD_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['amount' => 1250.50, 'storeOrderId' => 'ORDER-1001']),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status < 200 || $status >= 300) {
// handle the error
}
$order = json_decode($body, true);
header('Location: ' . $order['paymentUrl']);A new order returns HTTP 201. Sending the same storeOrderId again returns HTTP 200 with the same order, so it is safe to retry a request that timed out.
{
"orderId": "5b0d2a4e-3f71-4c1e-9a53-7d0f1d2c8e11",
"storeOrderId": "ORDER-1001",
"referenceCode": "QP-7F3K",
"amount": 1250.5,
"status": "pending",
"trxId": null,
"paidAt": null,
"receivedAmountPaisa": null,
"amountMismatch": false,
"matchMethod": null,
"returnUrl": "https://yourshop.com/thank-you",
"paymentUrl": "https://qrpaybd.com/pay/QP-7F3K"
}- orderId is QRPayBD's id for the order. Use it to check or cancel the order.
- referenceCode is a short code such as QP-7F3K that is shown on the payment page.
- status is pending, paid or cancelled.
- paymentUrl is where you send the customer.
- trxId, paidAt and matchMethod are filled in once the order is paid.
- receivedAmountPaisa and amountMismatch describe what the payment message said arrived (see step 3).
Step 2: send the customer to the payment page
Redirect the customer's browser to paymentUrl, or show it as a link. The page shows your wallet number, the exact amount to pay, the order reference to type into the wallet app and a box for the TrxID. It works in Bangla and English and updates by itself once the payment is confirmed.
Do not mark an order paid just because the customer returns to your site. Only a webhook, or a status check made from your server, confirms a payment.
Step 3: confirm the payment
When an order is paid, QRPayBD sends a POST request to your webhook address with a JSON body and the headers X-QRPayBD-Signature, X-QRPayBD-Event and X-QRPayBD-Delivery.
{
"event": "order.paid",
"orderId": "5b0d2a4e-3f71-4c1e-9a53-7d0f1d2c8e11",
"storeOrderId": "ORDER-1001",
"referenceCode": "QP-7F3K",
"amountPaisa": 125050,
"receivedAmountPaisa": 125050,
"amountMismatch": false,
"matchMethod": "trx_claim",
"trxId": "01M2N876B7",
"paidAt": "2026-09-26T09:14:22.000Z"
}Always verify the signature before you trust the request. The signature is sha256= followed by the HMAC-SHA256 of the raw request body, made with your webhook secret, in hexadecimal. Check it against the exact bytes you received, not a re-encoded copy.
import crypto from "node:crypto";
import express from "express";
const app = express();
// Use the RAW body: the signature covers the exact bytes that were sent.
app.post("/webhooks/qrpaybd", express.raw({ type: "application/json" }), (req, res) => {
const expected =
"sha256=" +
crypto.createHmac("sha256", process.env.QRPAYBD_WEBHOOK_SECRET).update(req.body).digest("hex");
const received = String(req.headers["x-qrpaybd-signature"] || "");
const ok =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
// 1. Find your order by event.storeOrderId. Skip it if it is already paid.
// 2. Compare event.amountPaisa with your own order total (in paisa).
// 3. If event.amountMismatch is true, hold the order for a manual check.
// 4. Mark the order paid and keep event.trxId.
res.status(200).json({ ok: true });
});$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('QRPAYBD_WEBHOOK_SECRET'));
$received = $_SERVER['HTTP_X_QRPAYBD_SIGNATURE'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// Same checks as above: find the order, skip if already paid,
// compare $event['amountPaisa'], look at $event['amountMismatch'], then mark it paid.
http_response_code(200);Then, in your handler:
- Find your order by storeOrderId, and ignore the event if it is already paid. The same delivery can arrive more than once.
- Compare amountPaisa (the order total in paisa, 100 paisa = 1 taka) with your own order total.
- If amountMismatch is true, the payment message showed a different amount from the order. Do not fulfil it automatically: check with the customer.
- Mark the order paid and keep trxId for your records.
- Reply quickly with any 2xx status. Do slow work afterwards.
QRPayBD does not retry a failed webhook delivery yet, and the webhook address must be a public https:// address. To be safe, also check your pending orders every few minutes with GET /orders/ORDER_ID and treat status paid the same way as the webhook.
Check or cancel an order
curl https://qrpaybd.com/orders/ORDER_ID \
-H "Authorization: Bearer YOUR_STORE_API_KEY"curl -X POST https://qrpaybd.com/orders/ORDER_ID/cancel \
-H "Authorization: Bearer YOUR_STORE_API_KEY"
# {"ok": true, "status": "cancelled"}Cancelling a pending order closes it: the payment page shows it as cancelled and a customer can no longer claim a payment for it. A paid order cannot be cancelled (409 order_already_paid). QRPayBD does not refund: refunds are made from your own wallet or bank.
Errors
- 400 validation_failed: a field is missing or invalid, for example amount is not a positive number or returnUrl is not an http(s) address.
- 401 invalid_api_key: the store API key is missing, wrong or revoked.
- 404: there is no such order on your account.
- 409 order_already_paid: the order is paid and cannot be cancelled.
- 429: too many requests. Wait a moment and try again.
- 5xx: a problem on our side. Try again after a short delay. Sending the same storeOrderId again is safe.
Error responses are JSON, for example {"error": "invalid_api_key"}.
Before you go live
- Keep the API key and the webhook secret on your server only.
- Verify every webhook signature and compare the amount.
- Handle repeated deliveries safely, using storeOrderId.
- Test the whole flow with a small real payment, for example Tk 1.
- Watch the Unmatched screen in your dashboard: payments that could not be matched automatically appear there.
Good to know
- Payments are confirmed from the payment SMS read by your Collector app, not from an official wallet API. If the phone is off or offline, or a message format is new to us, confirmation can be delayed or need a manual match in the dashboard.
- The message readers for bKash, Nagad and Upay have been verified on real payments; Rocket and banks are planned.
- The customer must pay the exact order amount. A different amount is not confirmed automatically.