Webhooks

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.

1

Go to Webhooks

Dashboard → Webhooks → Add Webhook

2

Enter your URL

Must be HTTPS in production. e.g. https://yoursite.com/webhook

3

Select events

Choose which payment events to subscribe to

4

Copy your secret

Shown once after creation — store it in an env variable

Open Webhooks Dashboard

Events

Subscribe to one or more of the following event types.

PAYMENT_PENDING

A new KHQR code has been generated and is waiting for payment.

Right after POST /api/v1/khqr/create succeeds

PAYMENT_SUCCESS

Customer has paid. Safe to fulfill the order.

When the Bakong network confirms the payment

PAYMENT_EXPIRED

Payment window closed without a completed payment.

When the QR session timer reaches zero

PAYMENT_FAILED

Payment 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.

Body Schema
eventstring

Event name — one of the four types above

dataobject

Event-specific payload (see examples below)

timestampnumber

Unix 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/json

Always 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.

JavaScript
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.

AttemptDelay before retryLog status
1stImmediatePENDING
2nd2 secondsRETRYING
3rd4 secondsRETRYING
4th8 secondsRETRYING
nth2ⁿ⁻¹ secondsRETRYING
FinalFAILED or SUCCESS

After 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