Getting Started: Concepts and Prerequisites

Table of Contents

  1. What you're building
  2. How a KHQR payment flows
  3. Key concepts
  4. A note on the package choice
  5. Prerequisites
  6. Beginner Pitfalls

1. What you're building

By the end of this series, you'll have a page that generates a scannable Bakong KHQR code and tells you the moment it's paid, using three steps:

  1. Generate a KHQR code — an EMVCo-format string any Bakong-participating banking app can scan and pay.
  2. Render it as a scannable image — the generated value is just text, not a picture.
  3. Check whether it's been paid — by polling the Bakong Open API with a hash of the QR.

The biggest thing to sit with before writing any code: step 1 needs no payment gateway account and no secret key. That's a real structural difference from a typical integration like this site's ABA PayWay Tutorial series, where every request has to be signed before PayWay will accept it.

2. How a KHQR payment flows

This is the flow the National Bank of Cambodia describes in its QR Payment Integration guide (PDF), with the QR generated on the merchant's backend — the same setup this series builds:

  1. The customer starts a payment on your website or app.
  2. Your backend generates the KHQR with NBC's KHQR SDK (or by following the EMVCo QR standard directly), then returns the QR, its MD5 hash, and its expiry time to your frontend.
  3. Your frontend shows the QR to the customer, along with when it expires.
  4. The customer scans it with the Bakong app or any bank app that supports KHQR — in practice, all Bakong member banks.
  5. The customer confirms the payment in their app.
  6. Your backend checks the payment status with Bakong's Open API, using the MD5 hash, on a repeating interval (you choose how often) until the QR expires.
  7. Bakong answers with the payment's status.
  8. You show the result to the customer, as a message on the page or a push notification.
Customer        Frontend          Your backend          Bakong
   |                |                   |                   |
   |-- start ------>|                   |                   |
   |                |-- create QR ----->|                   |
   |                |                   |-- generate KHQR   |
   |                |                   |   (offline, SDK)  |
   |                |<-- QR, md5, ------|                   |
   |                |    expiry         |                   |
   |<-- show QR ----|                   |                   |
   |                                                        |
   |-- scan & confirm in bank app ------------------------->|
   |                                                        |
   |                |-- check status -->|-- check by md5 -->|
   |                |                   |<-- status --------|
   |                |<-- paid / not ----|                   |
   |<-- "Paid!" ----|    yet (repeat)   |                   |

When your backend checks the status (step 6), handle each result differently:

Bakong saysWhat to do
SuccessMark the order paid and continue (confirm to the customer, print a receipt, etc.).
FailedMark the transaction failed and tell the customer.
Not found, QR not expired yetNot paid yet — keep checking.
Not found, QR expiredMark it timed out and ask the customer to pay again with a new QR.

For step 8, NBC describes two ways to get the result to the customer's screen: your backend pushes it to the frontend (e.g. over a WebSocket), or your frontend asks your backend repeatedly. This series uses the second, simpler option — see Part 3.

NBC also says a QR's expiry shouldn't exceed 10 minutes. If a payment still hasn't been confirmed by then, the fallback is a manual check: ask the customer to show the transaction in their Bakong or bank app.

3. Key concepts

TermWhat it means
KHQRCambodia's standardized QR payment format (built on the EMVCo QR standard), issued by the National Bank of Cambodia. Any participating bank/e-wallet app can scan it — it isn't tied to one bank.
Bakong Account IDThe identifier of who gets paid — looks like someone@devb. It is not secret; it functions like a bank account number, not a password.
KHQR string vs. KHQR imageGenerating a KHQR produces a plain text string in EMVCo format — not a picture. You run that string through a separate QR-image library to get something a phone camera can scan. This trips up almost everyone the first time.
Static vs. dynamic KHQRA KHQR with no amount (amount omitted/zero) is static — reusable, no expiry, the payer types in how much to pay. A KHQR with an amount set is dynamic — one specific charge — and it requires an expirationTimestamp. This series builds a dynamic KHQR.
MD5 hashGenerating a KHQR also produces an MD5 hash of the QR string. This hash isn't shown to the payer — it's what you use afterward to ask the Bakong API "has this specific QR been paid yet?"
Bearer token / access tokenA credential from the Bakong Open API portal that authorizes calls to check payment status. It's emailed to you after registering, expires after 90 days (NBC emails a reminder 3 days before), and is renewed from the same portal. Unlike the Account ID, this is sensitive — keep it server-side. Full steps in Part 3.

4. A note on the package choice

This series uses bakong-khqr, NBC's official KHQR SDK — the same package used in the National Bank of Cambodia's own KHQR SDK Document (PDF). Its source isn't developed in public (its npm listing points to NBC's own GitLab, which isn't publicly reachable), but the published package ships readable JavaScript, so you can open node_modules/bakong-khqr/src to see exactly what it does.

It ships no TypeScript types. If you're using TypeScript, the community-written @types/bakong-khqr package adds them, though it leaves the generate result (data) typed as unknown. There's also ts-khqr, an independent community reimplementation of the KHQR standard with its own types — it doesn't use bakong-khqr at all, so it isn't guaranteed to match the official SDK in every edge case.

Bakong's canonical docs are PDFs — the KHQR SDK Document above and the Bakong Open API Document (PDF) — unlike ABA PayWay's HTML developer portal, which makes a guide like this one easy to silently drift from as they're updated. Check them before shipping to production.

5. Prerequisites

  • A Bakong Account ID — the account a generated KHQR will pay into.
  • A Bakong Open API access token — register at api-bakong.nbc.gov.kh/register (it can take up to 24 hours to arrive by email, so do this first); only needed for the status-check step (Part 3), not for generating a KHQR.
  • A basic web page you can edit.
  • A backend you can run locally (this series' example uses Node.js/Express, but any language works).

6. Beginner Pitfalls

  • Don't assume generating a KHQR requires network access or an access token. It doesn't — only the status-check step does.
  • Don't confuse the Bakong Account ID (public, like a bank account number) with the access token (sensitive, server-side only). Mixing these up is an easy but consequential mistake.
  • Don't mix up bakong-khqr (NBC's official SDK, used here) with community packages like ts-khqr — their APIs differ, so code copied from a guide using one won't run with the other.

Buy Me a Coffee

Found this tutorial helpful? Consider buying me a coffee to support more free content!