Generating and Displaying a KHQR Code
ជំពូកនេះមិនទាន់ត្រូវបានបកប្រែជាភាសាខ្មែរនៅឡើយទេ — សូមអានជាភាសាអង់គ្លេសខាងក្រោម។
Table of Contents
- Why this whole step is offline
- Generate the KHQR string and MD5 hash
- Render it as a scannable image
- Display it on the frontend
- Beginner Pitfalls
1. Why this whole step is offline
Unlike a payment gateway that signs and submits a request server-side
(e.g. ABA PayWay's Purchase API),
generating a KHQR code involves no Bakong API call at all. The QR
payload itself just encodes who gets paid, how much, and until when — there
is nothing for a server to "create" on Bakong's side first. This example
still builds the KHQR server-side, but only for architectural consistency
(and to keep expirationTimestamp server-controlled) — not because
anything here needs protecting the way a signed request would.
npm install bakong-khqr qrcode
bakong-khqr is NBC's official KHQR SDK — the one used in the
KHQR SDK Document
— and builds the KHQR string; qrcode turns any string into a scannable
image.
2. Generate the KHQR string and MD5 hash
const { BakongKHQR, khqrData, IndividualInfo } = require('bakong-khqr');
const DEMO_AMOUNT = 5000; // 5,000 KHR, hardcoded for this teaching example
const EXPIRATION_MS = 5 * 60 * 1000; // 5 minutes
const expiresAt = Date.now() + EXPIRATION_MS;
const individualInfo = new IndividualInfo(
process.env.BAKONG_ACCOUNT_ID,
process.env.BAKONG_MERCHANT_NAME,
process.env.BAKONG_MERCHANT_CITY || 'Phnom Penh',
{
currency: process.env.BAKONG_CURRENCY === 'USD'
? khqrData.currency.usd
: khqrData.currency.khr,
amount: DEMO_AMOUNT,
expirationTimestamp: expiresAt,
},
);
const result = new BakongKHQR().generateIndividual(individualInfo);
if (result.status.code !== 0) {
return res.status(500).json({ error: result.status.message });
}
// result.data.qr -> the raw KHQR string (EMVCo format)
// result.data.md5 -> MD5 hash of that string, used in Part 3
new BakongKHQR() takes no arguments — generating a KHQR needs no access
token. The IndividualInfo constructor takes the Bakong Account ID,
merchant name, and city in that order, then an object of optional fields.
Generation doesn't throw on bad input — it returns
status.code: 1 with a message (and data: null), so always check
status.code before using result.data.
expirationTimestamp (in milliseconds, like Date.now()) is required
whenever amount is set (a "dynamic" KHQR) — leave it out and you get
"Expiration timestamp is required for dynamic KHQR". A KHQR with no
amount is "static" (reusable, payer enters the amount) and has no such
requirement.
KHR amounts are whole numbers (no decimals); USD amounts use decimals.
Passing the wrong shape for the selected currency produces a malformed
QR.
3. Render it as a scannable image
result.data.qr is plain text — a phone camera can't scan text directly.
Convert it to an image before it's any use to a payer:
const QRCode = require('qrcode');
const qrImage = await QRCode.toDataURL(result.data.qr); // data:image/png;base64,...
Unlike PayWay's Purchase API, which returns a ready-made QR PNG directly in its response, Bakong only ever gives you the raw string — rendering it is entirely on you.
Return everything the frontend needs:
res.json({
qrImage, // data URI, ready for an <img> tag
md5: result.data.md5, // needed to check payment status in Part 3
amount: DEMO_AMOUNT,
currency: process.env.BAKONG_CURRENCY || 'KHR',
expiresAt,
});
4. Display it on the frontend
<div id="result">Generating KHQR...</div>
<script>
async function createPayment() {
const resultEl = document.getElementById('result');
const res = await fetch('/create-payment', { method: 'POST' });
const data = await res.json();
if (data.error) {
resultEl.textContent = `Failed to generate KHQR: ${data.error}`;
return;
}
resultEl.innerHTML = `
<p>Scan with any Bakong-participating banking app:</p>
<img src="${data.qrImage}" alt="Bakong KHQR code" width="250" height="250">
<p>${data.amount.toLocaleString()} ${data.currency}</p>
<p id="status">Waiting for payment...</p>
`;
// pollPaymentStatus(data.md5, data.expiresAt) — see Part 3
}
createPayment();
</script>
Generation happens the moment the page loads, with no button click needed — unlike a gateway checkout, there's no "create a transaction" round trip here, just an offline string turned into an image.
5. Beginner Pitfalls
- Forgetting
expirationTimestampon a dynamic QR. Required the momentamountis set — omitting it fails generation. - Not checking
result.status.code. A failed generation returns an error status instead of throwing, so readingresult.data.qrwithout checking crashes onnull. - Treating the raw
qrstring as something you can display directly. It needs to go through a QR-image library first — see Section 3. - Assuming KHQR generation needs network access or an access token. It doesn't — only the status-check step in Part 3 does.
- Mixing up KHR and USD amount formatting. KHR is whole numbers, USD uses decimals — passing the wrong shape produces a malformed QR.
ទិញកាហ្វេជូនខ្ញុំ
ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!