bakong-open-api
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.
Requirements
- 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.
Installation
gem "bakong-open-api"
Or:
gem install bakong-open-api
require "bakong/open_api"
Quick start
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" }
Authentication
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]
Resources
client.tokens
| Method | Bakong endpoint | Returns |
|---|---|---|
.renew(email:) |
POST /v1/renew_token |
{ token: String } |
client.accounts
| Method | Bakong endpoint | Returns |
|---|---|---|
.exists?(account_id:) |
POST /v1/check_bakong_account |
true / false |
client.deeplinks
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.
client.transactions
| 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).
Error handling
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. # => "Not registered yet"
e.body # => { responseCode: 1, errorCode: 10, ... }
end
Specialized 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 |
Configuration
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"
)
Pairing with bakong-khqr
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)
end
Development
bin/setup # bundle install
bundle exec rspec
bundle exec rake # default task = spec
bin/console # IRB with bakong/open_api loaded
Releasing
bin/release v0.1.1
The 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.
Contributing
Issues and pull requests are welcome at https://github.com/VandyTheCoder/bakong-open-api-ruby.
Credits & disclaimer
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.
License
MIT — see LICENSE.txt.