Checking Payment Status
ជំពូកនេះមិនទាន់ត្រូវបានបកប្រែជាភាសាខ្មែរនៅឡើយទេ — សូមអានជាភាសាអង់គ្លេសខាងក្រោម។
Table of Contents
- Why you need this step
- The check-transaction call
- Polling from the frontend
- Testing checklist
- Beginner Pitfalls
1. Why you need this step
Everything in Part 2 happens offline — generating a KHQR and rendering it as an image tells you nothing about whether anyone actually paid it. This is the one step in the whole flow that talks to Bakong's real infrastructure, using the MD5 hash from Part 2 to ask "has this specific QR been paid yet?"
2. The check-transaction call
Confirmed against the Bakong Open API Document and NBC's QR Payment Integration guide:
router.post('/check-payment-status', async (req, res) => {
const { md5 } = req.body;
const bakongRes = await fetch(`${process.env.BAKONG_API_BASE_URL}/check_transaction_by_md5`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.BAKONG_ACCESS_TOKEN}`,
},
body: JSON.stringify({ md5 }),
});
if (bakongRes.status === 401) {
return res.status(500).json({ error: 'Bakong access token is invalid or expired' });
}
if (bakongRes.status === 404) {
return res.json({ status: 'pending' });
}
const data = await bakongRes.json();
if (data.responseCode === 0 && data.data) {
return res.json({ status: 'paid', transaction: data.data });
}
if (data.responseMessage === 'Transaction failed.') {
return res.json({ status: 'failed' });
}
res.json({ status: 'pending' });
});
This series runs against Bakong's production Open API, so set the base URL
in your backend's .env:
BAKONG_API_BASE_URL=https://api-bakong.nbc.gov.kh/v1
BAKONG_ACCESS_TOKEN=your-access-token
Bakong answers in one of three ways:
Success — paid:
{
"responseCode": 0,
"responseMessage": "Getting transaction successfully.",
"data": {
"hash": "8465d722d7d5...",
"fromAccountId": "customer@bank",
"toAccountId": "you@bank",
"currency": "USD",
"amount": 1.0,
"description": "..."
}
}
Failed — the payment went through the customer's app but failed:
{ "data": null, "errorCode": 1, "responseCode": 1, "responseMessage": "Transaction failed." }
Not found — nobody has paid this QR yet:
{ "data": null, "errorCode": 1, "responseCode": 1, "responseMessage": "Transaction could not be found. Please check and try again." }
Failed and not found share the same responseCode and errorCode (both
1), so the only way to tell them apart is responseMessage — which is
why the code above compares that string. Treat anything that isn't clearly
success or failed as "still pending", so an unexpected message just means
you keep checking.
The success response also tells you who paid how much to whom. Before
marking a real order paid, it's worth checking that data.amount,
data.currency, and data.toAccountId match the order you created.
Getting and renewing the access token
- Register at api-bakong.nbc.gov.kh/register with your organization, project name, and email.
- Verify your email. NBC emails you a confirmation code; enter it to verify.
- Wait for the token email. After verification, the token arrives
from
no.reply@nbc.gov.kh, within 24 hours. Paste it into.envasBAKONG_ACCESS_TOKEN. - Note the expiry date. Tokens expire 90 days after issue; the exact Expire Date is in the email. NBC emails you a reminder 3 days before it expires.
- Renew when it expires. On the register page, click Token
expired? Renew, enter the same email, and a new token (with a new
expiry date) is emailed to you. Swap it into
.envand restart your backend.
When the 3-day reminder email arrives, renew straight away. Don't wait for the token to expire: an expired token makes every status check fail, so paid orders will look unpaid.
The token is also limited to 100 requests per day (the limit resets daily), which matters for polling — see the note in section 3.
Whichever access token you use here must only ever live on your backend/server, never in browser JavaScript, and never committed to a git repo — the Account ID from Part 1 is fine to be public; this token is not.
3. Polling from the frontend
async function pollPaymentStatus(md5, expiresAt) {
const statusEl = document.getElementById('status');
const POLL_INTERVAL_MS = 3000;
while (Date.now() < expiresAt) {
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
const res = await fetch('/check-payment-status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ md5 }),
});
const data = await res.json();
if (data.status === 'paid') {
statusEl.textContent = 'Payment received!';
return;
}
if (data.status === 'failed') {
statusEl.textContent = 'Payment failed. Refresh the page to try again.';
return;
}
}
statusEl.textContent = 'This KHQR has expired. Refresh the page for a new one.';
}
Kick this off right after displaying the QR image
(Part 2),
and stop as soon as you get a paid or failed result or the QR's own
expirationTimestamp passes — a dynamic KHQR is only valid until then
regardless of how long you keep polling.
Mind the 100-requests-per-day limit. Every poll here is one Bakong API call. At one poll every 3 seconds, a single QR left open for 5 minutes uses all 100 requests for the day. Fine for trying things out; for anything with real traffic, poll less often and keep QR expiry short.
4. Testing checklist
- Loading the page generates a KHQR image with no errors (this needs no network access, so it should work even with a placeholder access token).
- The image is actually scannable by a QR-reading app, not just visually present.
-
/check-payment-statusreturns{ status: 'pending' }rather than crashing, given a valid but unpaidmd5. - The page correctly shows an "expired" state once
expirationTimestamppasses without payment. - A small real payment (e.g. ៛100) is detected as paid by
/check-payment-status.
5. Beginner Pitfalls
- Putting the Bakong access token in frontend JavaScript. Like any API credential, it must stay server-side.
- Polling forever. Stop once you get a paid result or the QR expires —
don't poll past
expirationTimestamp. With a 100-requests-per-day limit, runaway polling can lock you out until the next day. - Letting the token expire unnoticed. Bakong starts answering
401and no payment can be confirmed. Tokens last 90 days — renew when NBC's 3-day reminder email arrives. - Treating "failed" like "not found". Both come back with
responseCode: 1; onlyresponseMessagediffers. If you treat them the same, a failed payment leaves the customer waiting until the QR expires. - Trying to check a static KHQR by MD5.
check_transaction_by_md5only works for dynamic KHQRs (ones with an amount), like the one this series builds. - Assuming a 404 means an error. A 404 from
check_transaction_by_md5is an expected "not paid yet" response, not a failure to handle specially. - Forgetting that test payments are real money. This runs against production, so every test payment actually moves funds — keep test amounts tiny.
ទិញកាហ្វេជូនខ្ញុំ
ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!