bakong-open-api

Gem Version

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
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.response_message # => "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.