Real-time
Payment Events
Instead of polling the check endpoint, register a webhook URL and receive instant HTTP notifications whenever a payment state changes.
Instant delivery
POST within seconds of the event
HMAC signed
SHA-256 signature on every request
Auto retry
Exponential backoff up to 10 attempts
Register a Webhook
Create and manage webhooks from the dashboard. Your API key must have the webhook permission.
Go to Webhooks
Dashboard → Webhooks → Add Webhook
Enter your URL
Must be HTTPS in production. e.g. https://yoursite.com/webhook
Select events
Choose which payment events to subscribe to
Copy your secret
Shown once after creation — store it in an env variable
Events
Subscribe to one or more of the following event types.
PAYMENT_PENDINGA new KHQR code has been generated and is waiting for payment.
Right after POST /api/v1/khqr/create succeeds
PAYMENT_SUCCESSCustomer has paid. Safe to fulfill the order.
When the Bakong network confirms the payment
PAYMENT_EXPIREDPayment window closed without a completed payment.
When the QR session timer reaches zero
PAYMENT_FAILEDPayment was attempted but did not complete successfully.
On a payment error or rejection
Payload Format
Every webhook delivers a JSON body with three top-level fields.
eventstringEvent name — one of the four types above
dataobjectEvent-specific payload (see examples below)
timestampnumberUnix milliseconds when the event was fired
{
"event": "PAYMENT_SUCCESS",
"data": {
"transactionId": "TXN-abc123...",
"md5": "a1b2c3d4e5f6...",
"amount": 25.00,
"currency": "USD",
"merchantName": "FINN SHOP",
"hash": "0xabc123..."
},
"timestamp": 1749372860000
}Request Headers
Every delivery includes these HTTP headers on the POST request to your endpoint.
Content-Typeapplication/jsonAlways JSON
X-BakongPay-Signature<hex string>HMAC-SHA256 of the raw body, signed with your webhook secret
X-BakongPay-Event-Attempt1, 2, 3 …Current delivery attempt number (starts at 1)
Verify the Signature
Always validate X-BakongPay-Signature before processing an event. The signature is HMAC-SHA256 of the exact raw request body, using your webhook secret as the key.
Use timingSafeEqual (not ===) to compare the signature — plain string comparison is vulnerable to timing attacks.
const crypto = require("crypto");
const express = require("express");
const app = express();
// Must use raw body for signature verification
app.use(express.raw({ type: "application/json" }));
app.post("/webhook", (req, res) => {
const rawBody = req.body.toString("utf-8");
const sig = req.headers["x-bakongpay-signature"];
const secret = process.env.WEBHOOK_SECRET; // from dashboard
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const valid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sig ?? "")
);
if (!valid) {
return res.status(401).json({ error: "Invalid signature" });
}
const { event, data, timestamp } = JSON.parse(rawBody);
switch (event) {
case "PAYMENT_SUCCESS":
console.log("Payment confirmed:", data.transactionId);
// fulfill order, send confirmation email, etc.
break;
case "PAYMENT_EXPIRED":
console.log("QR expired:", data.transactionId);
break;
}
res.status(200).json({ received: true });
});
app.listen(3000);Retry Behavior
If your endpoint returns a non-2xx status, times out (>10s), or is unreachable, the delivery is automatically retried with exponential backoff. You set the maximum retry count (1–10) when creating the webhook.
1stImmediatePENDING2nd2 secondsRETRYING3rd4 secondsRETRYING4th8 secondsRETRYINGnth2ⁿ⁻¹ secondsRETRYINGFinal—FAILED or SUCCESSAfter all retries are exhausted the log is marked FAILED. You can view delivery history for each webhook in the dashboard.
Requirements
API key permission
Your API key must include the "webhook" permission. Set this when creating or editing the key in the dashboard.
Respond within 10 seconds
Your endpoint must return HTTP 2xx within 10 seconds. Offload slow work (DB writes, emails) to a background queue after responding.
HTTPS in production
Webhook URLs must use HTTPS so the payload is encrypted in transit. HTTP is accepted only for local testing.
Idempotency
Handle duplicate deliveries gracefully — retries send the same event. Use transactionId as an idempotency key (e.g. "process only if status is not already SUCCESS").
API Reference
Generate KHQR codes and check payments
SDK Examples
JS, Python, PHP, Next.js integrations