A Ruby client for the Bakong Open API — the National Bank of Cambodia's HTTP API for verifying KHQR transactions, checking Bakong account existence, generating wallet deep links, and managing access tokens.
⚠️ Unofficial client. This gem is not an official SDK from the National Bank of Cambodia or any Bakong-affiliated entity. It is an independent, community-maintained Ruby wrapper implemented against the publicly available API documentation at https://api-bakong.nbc.gov.kh/document. The NBC has not reviewed, endorsed, or sponsored this project. Always verify behavior against the upstream documentation before relying on it in production.
Zero runtime gem dependencies — uses only the Ruby standard library
(Net::HTTP). Pairs naturally with bakong-khqr
for generating the KHQR payloads you then verify.
- Ruby
>= 3.4.1 - A registered Bakong developer email (request one via the NBC's developer
portal). The email is used to mint short-lived JWT access tokens via
POST /v1/renew_token.
gem "bakong-open-api"Or:
gem install bakong-open-apirequire "bakong/open_api"client = Bakong::OpenApi.client
# 1. Mint a token using your registered email
client.token = client.tokens.renew(email: "you@example.com").fetch(:token)
# 2. Check whether a Bakong account exists
client.accounts.exists?(account_id: "vandy@aclb")
# => true | false
# 3. Verify a transaction by the MD5 of its KHQR string
client.transactions.check_by_md5(md5: "d60f3db96913029a2af979a1662c1e72")
# => { hash: "...", from_account_id: "...", to_account_id: "...",
# currency: "USD", amount: 1.0, description: "",
# created_date_ms: 1605774370608.0, acknowledged_date_ms: 1605774422421.0 }
# nil if the API returns errorCode 1 (not found)
# 4. Generate a wallet deep link for a KHQR
client.deeplinks.generate(qr: "00020101...")
# => { short_link: "https://bakong.link/abc" }Every endpoint except tokens.renew and deeplinks.generate requires a
Bearer token. Tokens are short-lived JWTs (~90 days at the time of writing).
Two ways to use them:
# Construct with token
client = Bakong::OpenApi.client(token: "eyJhbGciOiJ...")
# Or set/replace after construction (e.g. after refresh)
client.token = "eyJhbGciOiJ..."To mint a fresh token:
response = client.tokens.renew(email: "vandysodanheang@gmail.com")
client.token = response[:token]| Method | Bakong endpoint | Returns |
|---|---|---|
.renew(email:) |
POST /v1/renew_token |
{ token: String } |
| Method | Bakong endpoint | Returns |
|---|---|---|
.exists?(account_id:) |
POST /v1/check_bakong_account |
true / false |
source = Bakong::OpenApi::SourceInfo.new(
app_icon_url: "https://yourapp.example/icon.png",
app_name: "Your App",
app_deep_link_callback: "yourapp://payment-result"
)
client.deeplinks.generate(qr: "00020101...", source_info: source)
# => { short_link: "..." }source_info is optional; when provided, all three fields are required
and the gem raises Bakong::OpenApi::MissingFieldsError before the HTTP
call if any are blank.
| Method | Bakong endpoint | Returns |
|---|---|---|
.check_by_md5(md5:) |
POST /v1/check_transaction_by_md5 |
transaction Hash or nil |
.check_by_hash(hash:) |
POST /v1/check_transaction_by_hash |
transaction Hash or nil |
.check_by_short_hash(hash:, amount:, currency:) |
POST /v1/check_transaction_by_short_hash |
transaction Hash or nil |
.check_by_instruction_ref(instruction_ref:) |
POST /v1/check_transaction_by_instruction_ref |
transaction Hash or nil |
.check_by_external_ref(external_ref:) |
POST /v1/check_transaction_by_external_ref |
transaction Hash or nil |
.check_by_md5_list(md5s:) |
POST /v1/check_transaction_by_md5_list |
full envelope Hash (data is per-row Array) |
.check_by_hash_list(hashes:) |
POST /v1/check_transaction_by_hash_list |
full envelope Hash (data is per-row Array) |
Single lookups return the transaction Hash with snake_cased keys on
success, nil when the API reports errorCode 1 (transaction not found), and
raise on every other failure. Batch lookups are capped at 50 items and
return the full envelope so callers can iterate per-row statuses (SUCCESS,
NOT_FOUND, FAILED, STATIC_QR).
All errors inherit from Bakong::OpenApi::Error and carry the HTTP status,
the Bakong responseCode / errorCode / responseMessage, and the raw
response body:
begin
client.tokens.renew(email: "wrong@example.com")
rescue Bakong::OpenApi::NotRegisteredError => e
e.error_code # => 10
e.response_message # => "Not registered yet"
e.body # => { responseCode: 1, errorCode: 10, ... }
endSpecialized error classes for each Bakong errorCode:
| errorCode | Class | Meaning |
|---|---|---|
| 1 | TransactionNotFoundError |
Transaction not found (also returned as nil from single lookups) |
| 2 | StaticQrNotSupportedError |
Static QR codes aren't supported by this endpoint |
| 3 | TransactionFailedError |
The transaction failed |
| 4 | DeeplinkProviderError |
Upstream deep link provider error |
| 5 | MissingFieldsError |
Required request fields missing |
| 6 | UnauthorizedError |
Token missing or rejected |
| 7 | EmailServerDownError |
Bakong couldn't send the email |
| 8 | EmailAlreadyRegisteredError |
Email already registered |
| 9 | BakongUnreachableError |
Bakong reports it can't reach its backend |
| 10 | NotRegisteredError |
This email isn't registered |
| 11 | AccountNotFoundError |
Bakong account doesn't exist (also returned as false from accounts.exists?) |
| 12 | AccountInvalidError |
Bakong account ID malformed |
Transport-level errors map to:
| HTTP status | Class |
|---|---|
| 401 | AuthenticationError |
| 403 | TokenExpiredError |
| 404 | NotFoundError |
| 429 | RateLimitError |
| 4xx | InvalidRequestError |
| 5xx | ServerError |
| socket / DNS failure | ConnectionError |
client = Bakong::OpenApi.client(
token: "eyJhbGciOiJ...",
base_url: "https://api-bakong.nbc.gov.kh", # default
open_timeout: 45, # seconds
read_timeout: 45,
user_agent: "myapp/1.2.3"
)The typical flow when accepting Bakong payments is:
require "bakong/khqr"
require "bakong/open_api"
# Generate a KHQR string for the buyer to scan
info = Bakong::Khqr::IndividualInfo.new(
bakong_account_id: "vandy@aclb",
merchant_name: "Sodanheang Coffee",
merchant_city: "Phnom Penh",
amount: 1.50,
currency: Bakong::Khqr::CURRENCY[:usd],
expiration_timestamp: (Time.now.to_f * 1000).to_i + 5 * 60 * 1000
)
qr_data = Bakong::Khqr.generate_merchant(info)
qr_data[:qr] # → display this as a QR image
qr_data[:md5] # → store this; poll it later
# Later (e.g. via a background job), check whether the buyer paid
client = Bakong::OpenApi.client(token: ENV.fetch("BAKONG_TOKEN"))
transaction = client.transactions.check_by_md5(md5: qr_data[:md5])
if transaction
# paid — fulfill the order
else
# not yet paid (or expired)
endThe bakong- prefix is used as an umbrella namespace for Cambodian
payment-system gems — the same convention as aws-sdk-* and
google-cloud-*. Each bakong-* gem is independent and installable on its
own, but they share the top-level Bakong:: module so they compose cleanly
in the same codebase. Today the family is bakong-khqr
and bakong-open-api; more may follow as the Bakong ecosystem grows.
bin/setup # bundle install
bundle exec rspec
bundle exec rake # default task = spec
bin/console # IRB with bakong/open_api loadedbin/release v0.1.1The script bumps VERSION, runs RSpec, builds the gem, commits + tags,
prompts for your RubyGems MFA OTP, pushes the gem to RubyGems, pushes the
git tag, and creates a GitHub release. Requires $RUBY_GEM_KEY in your
shell and an authenticated gh CLI.
Issues and pull requests are welcome at https://github.com/VandyTheCoder/bakong-open-api-ruby.
This is an independent, unofficial Ruby client built against the public Bakong Open API documentation published by the National Bank of Cambodia. The NBC has not reviewed, endorsed, or sponsored this project. All trademarks and API specifications remain the property of the National Bank of Cambodia.
MIT — see LICENSE.txt.