Generate styled Bakong KHQR card images with correct USD/KHR amount formatting.
Project description
bakong-khqr-image
Generate styled Bakong KHQR card images that match the official NBC design — with correct USD and KHR amount formatting.
Built from production experience: the official
bakong-khqrlibrary formats USD amounts with a comma (0,01 USD) instead of a period (0.01 USD). This library fixes that and ships a clean, standalone implementation.
Features
- ✅ EMV-compliant KHQR payload generation per KHQR spec v1.3 (no external bakong dependency)
- ✅ Correct amount formatting —
1,234.56USD ·4,000KHR - ✅ Styled 300 × 450 px card (Bakong red header, merchant info, dashed separator, rounded corners)
- ✅ Bundled assets — works out of the box, no manual font/icon setup
- ✅ Multiple export formats: PNG · JPEG · WebP · bytes · Base64 · data URI
- ✅ Static (open-amount) and dynamic QR code support
- ✅ Individual & merchant account types — correct Tag 15 / Tag 29 structure
- ✅ Tag 99 transaction timestamps — required for Bakong backend to process payments
- ✅
from_emv()— render a styled card from any existing raw KHQR string - ✅ Typed, documented, tested
Installation
pip install bakong-khqr-image
Dependencies (Pillow and qrcode) are installed automatically.
Quick Start
from bakong_khqr_image import KHQRImage
# Create one instance per merchant — reuse it for every payment
khqr = KHQRImage(
bakong_account_id="yourname@aclb",
merchant_name="My Coffee Shop",
merchant_city="Phnom Penh", # optional, default "Phnom Penh"
currency="USD", # optional, default "USD"
account_type="individual", # optional, default "individual" (personal wallet)
# use "merchant" for registered KHQR merchants
)
# Generate a styled QR card image
result = khqr.generate_image(amount=2.50)
# Save as PNG
result.save_png("payment_qr.png")
# Or get bytes / Base64 / data URI for web / API responses
png_bytes = result.to_bytes()
b64 = result.to_base64()
data_uri = result.to_data_uri()
API Reference
KHQRImage
KHQRImage(
bakong_account_id: str,
merchant_name: str,
merchant_city: str = "Phnom Penh",
currency: str = "USD", # "USD" or "KHR"
account_type: str = "individual", # "individual" or "merchant"
)
generate_image(amount, *, bill_number, store_label, terminal_label, mobile_number, static, include_additional_data, ttl_seconds) → QRImageResult
Renders the full styled card image.
| Parameter | Type | Default | Description |
|---|---|---|---|
amount |
float |
Transaction amount. 0 = open-amount / static QR |
|
bill_number |
str |
None |
Unique reference ID (auto-generated UUID if omitted) |
store_label |
str |
None |
Store label shown in the Bakong app |
terminal_label |
str |
None |
Terminal identifier |
mobile_number |
str |
None |
Merchant phone number |
static |
bool |
False |
True → static QR (no amount, no Tag 99) |
include_additional_data |
bool |
False |
True → embed Tag 62 (bill/store/terminal) in payload |
ttl_seconds |
int |
60 |
Transaction validity window for Tag 99. 0 = omit |
generate_payload(amount, *, ...) → (payload, bill_number)
Returns only the raw EMV KHQR string — useful for custom renderers or the Bakong payment-verification API.
payload, ref_id = khqr.generate_payload(amount=5.00)
md5_hash = KHQRImage.md5(payload) # pass to Bakong API to check payment status
KHQRImage.from_emv(emv_string) → QRImageResult
Generate a styled card image directly from an existing raw KHQR / EMV string. Merchant name, amount, and currency are extracted automatically from Tags 59, 54, and 53.
emv = "00020101021229240020yourname@aclb...6304ABCD"
result = KHQRImage.from_emv(emv)
result.save_png("from_emv.png")
KHQRImage.md5(payload) → str
Computes the MD5 hash of a payload string required by the Bakong transaction-check API.
QRImageResult
| Method | Returns | Description |
|---|---|---|
save_png(path) |
str |
Save PNG, returns absolute path |
save_jpeg(path) |
str |
Save JPEG, returns absolute path |
save_webp(path) |
str |
Save lossless WebP, returns absolute path |
to_bytes() |
bytes |
PNG-encoded bytes |
to_base64() |
str |
Base64-encoded PNG string |
to_data_uri() |
str |
data:image/png;base64,… URI |
.image |
PIL.Image |
Raw PIL Image for custom processing |
Examples
KHR currency
from bakong_khqr_image import KHQRImage
khqr = KHQRImage(
bakong_account_id="yourname@aclb",
merchant_name="ហាងកាហ្វេ",
currency="KHR",
)
result = khqr.generate_image(amount=4_000)
result.save_png("payment_khr.png")
Static (open-amount) QR
# Customer enters the amount inside the Bakong app — useful for printed menus
result = khqr.generate_image(amount=0, static=True)
result.save_png("static_qr.png")
Individual vs. merchant account type
# Default — personal Bakong wallet (Tag 29 only, no Tag 15)
khqr_personal = KHQRImage(
bakong_account_id="yourname@aclb",
merchant_name="My Shop",
account_type="individual", # default, can be omitted
)
# Registered KHQR merchant (adds Tag 15 with AID per KHQR spec v1.3)
khqr_merchant = KHQRImage(
bakong_account_id="merchant@wbkh",
merchant_name="Virackbot Mart",
account_type="merchant",
)
Render a card from an existing KHQR string
# No need to re-generate — just pass the raw EMV string
emv = "00020101021229240020yourname@aclb...6304ABCD"
result = KHQRImage.from_emv(emv)
result.save_png("from_emv.png")
Web / API response
result = khqr.generate_image(amount=1.50)
# Flask / Django — return as JSON
response = {"qr_image": result.to_data_uri()}
# Or stream PNG bytes directly
return Response(result.to_bytes(), mimetype="image/png")
Check payment status with the Bakong API
import requests
payload, ref_id = khqr.generate_payload(amount=5.00)
md5 = KHQRImage.md5(payload)
resp = requests.post(
"https://api-bakong.nbc.org.kh/v1/check_transaction_by_md5",
headers={"Authorization": "Bearer <YOUR_TOKEN>"},
json={"md5": md5},
)
if resp.json().get("responseCode") == 0:
print("Payment received!")
Access the raw PIL image
result = khqr.generate_image(amount=2.50)
pil_image = result.image # PIL.Image.Image
pil_image.show() # open in system viewer
📁 See the
examples/folder for full runnable scripts:
basic_usage.py— core workflowkhr_and_static.py— KHR & static QRaccount_types.py— individual vs. merchantfrom_emv.py— render from existing EMV stringadvanced_options.py— Tag 62, TTL, all export formatsweb_api_example.py— Flask / Django integration
Card Layout
┌─────────────────────────────┐ ◥ red fold
│ [KHQR / Bakong logo] │ ← red header (60 px)
│ │
│ Merchant Name │
│ 2.50 USD │
│ - - - - - - - - - - - - - - │ ← dashed separator
│ │
│ ┌───────────────────┐ │
│ │ │ │
│ │ QR Code │ │
│ │ [💲] │ │ ← currency icon overlay
│ │ │ │
│ └───────────────────┘ │
│ │
└─────────────────────────────┘
300 × 450 px
Sample image
Running Tests
pip install -e ".[dev]"
pytest
Publishing to PyPI
pip install build twine
python -m build
twine upload dist/*
License
MIT © 2026 Virackbot
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bakong_khqr_image-0.1.4.tar.gz.
File metadata
- Download URL: bakong_khqr_image-0.1.4.tar.gz
- Upload date:
- Size: 210.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf67d7056a4503c3805805e3fed2df654a5bc3fc4bdcf657003848cc3e1f6c1c
|
|
| MD5 |
4932ccd4f96f6fde5159c5bd2a2728b9
|
|
| BLAKE2b-256 |
418e5bd5cd7202779f17f732e20215c8399dd9d905cb261178060f161afb58ca
|
Provenance
The following attestation bundles were made for bakong_khqr_image-0.1.4.tar.gz:
Publisher:
publish-pypi.yml on Virackbot/bakong-khqr-image
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bakong_khqr_image-0.1.4.tar.gz -
Subject digest:
bf67d7056a4503c3805805e3fed2df654a5bc3fc4bdcf657003848cc3e1f6c1c - Sigstore transparency entry: 1037109885
- Sigstore integration time:
-
Permalink:
Virackbot/bakong-khqr-image@a75eedf1d9d1af43584f1781221249fe1d010b22 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Virackbot
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@a75eedf1d9d1af43584f1781221249fe1d010b22 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file bakong_khqr_image-0.1.4-py3-none-any.whl.
File metadata
- Download URL: bakong_khqr_image-0.1.4-py3-none-any.whl
- Upload date:
- Size: 204.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e3d088800f012aae9a5475c8bc399d0d0cea3a6ad9105cebbbf2f31aeb38e1f4
|
|
| MD5 |
a93aad2af44e99e833ac106cdfaa7aae
|
|
| BLAKE2b-256 |
d2319345b52ff03cb66ebae248b4b5253f1c25c9b20dd479f284871d93606fff
|
Provenance
The following attestation bundles were made for bakong_khqr_image-0.1.4-py3-none-any.whl:
Publisher:
publish-pypi.yml on Virackbot/bakong-khqr-image
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bakong_khqr_image-0.1.4-py3-none-any.whl -
Subject digest:
e3d088800f012aae9a5475c8bc399d0d0cea3a6ad9105cebbbf2f31aeb38e1f4 - Sigstore transparency entry: 1037109997
- Sigstore integration time:
-
Permalink:
Virackbot/bakong-khqr-image@a75eedf1d9d1af43584f1781221249fe1d010b22 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Virackbot
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@a75eedf1d9d1af43584f1781221249fe1d010b22 -
Trigger Event:
workflow_dispatch
-
Statement type: