API Documentation
Complete reference for the KHQR Gateway REST API.
Getting Started
The KHQR Gateway API allows you to create KHQR payment charges, track payments in real time, and receive webhook notifications. All endpoints return JSON. Authentication uses Bearer tokens passed in the Authorization header.
AUTH Authentication
Include your API key in the Authorization header for all requests.
Authorization: Bearer kh_live_your_api_key_here| Parameter | Value | Description |
|---|---|---|
Authorization | Bearer {api_key} | Your API key prefixed with "Bearer " |
BASE Base URL
All API requests should be made to this base URL.
https://your-domain.comPOST /api/v1/charges
Create a new KHQR payment charge. Returns a QR code, deep link, and payment details.
| Parameter | Type | Description |
|---|---|---|
amount | number * required | Payment amount in USD (minimum $0.01) |
currency | string optional | Currency code (default: "USD") |
description | string optional | Description of the payment |
metadata | object optional | Custom key-value metadata |
curl -X POST https://your-domain.com/api/v1/charges \
-H "Authorization: Bearer kh_live_abc123" \
-H "Content-Type: application/json" \
-d '{
"amount": 10.00,
"currency": "USD",
"description": "Order #1234",
"metadata": {
"order_id": "1234",
"customer_email": "test@example.com"
}
}'{
"id": "chg_a1b2c3d4e5f6",
"amount": 10.00,
"currency": "USD",
"description": "Order #1234",
"status": "pending",
"qr_code": "00020101021...",
"qr_image": "https://api.khqr.gateway/qr/chg_a1b2c3d4e5f6.png",
"deep_link": "https://bakong.nbc.gov.kh/r?id=...",
"md5_hash": "e99a18c428cb38d5f260853678922e03",
"bill_number": "BILL-20260419-XXXX",
"metadata": {
"order_id": "1234",
"customer_email": "test@example.com"
},
"created_at": "2026-04-19T10:00:00Z",
"expires_at": "2026-04-19T10:15:00Z"
}GET /api/v1/charges/:id
Retrieve the current status and details of a charge.
| Parameter | Type | Description |
|---|---|---|
id | string * required | Charge ID (path parameter) |
Possible status values:pendingpaidexpiredfailed
curl https://your-domain.com/api/v1/charges/chg_a1b2c3d4e5f6 \
-H "Authorization: Bearer kh_live_abc123"{
"id": "chg_a1b2c3d4e5f6",
"amount": 10.00,
"currency": "USD",
"description": "Order #1234",
"status": "paid",
"qr_code": "00020101021...",
"deep_link": "https://bakong.nbc.gov.kh/r?id=...",
"md5_hash": "e99a18c428cb38d5f260853678922e03",
"bill_number": "BILL-20260419-XXXX",
"paid_at": "2026-04-19T10:05:32Z",
"created_at": "2026-04-19T10:00:00Z",
"expires_at": "2026-04-19T10:15:00Z"
}POST /api/v1/charges/:id/check
Force a check with Bakong to get the latest payment status for a charge.
| Parameter | Type | Description |
|---|---|---|
id | string * required | Charge ID (path parameter) |
curl -X POST https://your-domain.com/api/v1/charges/chg_a1b2c3d4e5f6/check \
-H "Authorization: Bearer kh_live_abc123"{
"id": "chg_a1b2c3d4e5f6",
"status": "paid",
"checked_at": "2026-04-19T10:06:00Z",
"message": "Payment confirmed by Bakong"
}GET /api/v1/charges
List all charges with optional filtering and pagination.
| Parameter | Type | Description |
|---|---|---|
status | string optional | Filter by status: pending, paid, expired, failed |
limit | number optional | Results per page (default: 20, max: 100) |
offset | number optional | Offset for pagination (default: 0) |
curl "https://your-domain.com/api/v1/charges?status=paid&limit=10&offset=0" \
-H "Authorization: Bearer kh_live_abc123"{
"data": [
{
"id": "chg_a1b2c3d4e5f6",
"amount": 10.00,
"status": "paid",
"created_at": "2026-04-19T10:00:00Z"
}
],
"total": 42,
"limit": 10,
"offset": 0
}POST /api/v1/merchant/register
Register a new merchant account. Returns an API key for authentication.
| Parameter | Type | Description |
|---|---|---|
email | string * required | Merchant email address |
name | string * required | Merchant display name |
curl -X POST https://your-domain.com/api/v1/merchant/register \
-H "Content-Type: application/json" \
-d '{
"email": "merchant@example.com",
"name": "My Store"
}'{
"id": "mer_x1y2z3",
"email": "merchant@example.com",
"name": "My Store",
"api_key": "kh_live_new_api_key_here",
"created_at": "2026-04-19T10:00:00Z"
}POST /api/v1/merchant/webhook
Configure the webhook URL where payment events will be sent. Returns a webhook secret for signature verification.
| Parameter | Type | Description |
|---|---|---|
url | string * required | HTTPS URL to receive webhook events |
curl -X POST https://your-domain.com/api/v1/merchant/webhook \
-H "Authorization: Bearer kh_live_abc123" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/khqr"
}'{
"url": "https://example.com/webhooks/khqr",
"webhook_secret": "whsec_abc123def456",
"updated_at": "2026-04-19T10:00:00Z"
}POST /api/v1/merchant/webhook/test
Send a test webhook event to your configured URL to verify delivery.
curl -X POST https://your-domain.com/api/v1/merchant/webhook/test \
-H "Authorization: Bearer kh_live_abc123"{
"success": true,
"status_code": 200,
"response_time_ms": 245,
"message": "Test webhook delivered successfully"
}GET /api/v1/merchant/stats
Get transaction statistics for your merchant account.
curl https://your-domain.com/api/v1/merchant/stats \
-H "Authorization: Bearer kh_live_abc123"{
"total_charges": 1284,
"total_paid": 956,
"total_amount": 47820.50,
"pending_charges": 12,
"expired_charges": 316,
"average_amount": 50.00
}POST /api/v1/merchant/api-keys
Create a new API key for your merchant account.
| Parameter | Type | Description |
|---|---|---|
name | string * required | A descriptive name for the key |
environment | string optional | "live" or "test" (default: "live") |
curl -X POST https://your-domain.com/api/v1/merchant/api-keys \
-H "Authorization: Bearer kh_live_abc123" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Key",
"environment": "live"
}'{
"id": "key_abc123",
"name": "Production Key",
"key": "kh_live_new_key_xyz789",
"environment": "live",
"created_at": "2026-04-19T10:00:00Z"
}GET /api/v1/merchant/api-keys
List all API keys for your merchant account.
curl https://your-domain.com/api/v1/merchant/api-keys \
-H "Authorization: Bearer kh_live_abc123"{
"data": [
{
"id": "key_abc123",
"name": "Production Key",
"environment": "live",
"key_prefix": "kh_live_...xyz",
"created_at": "2026-04-19T10:00:00Z"
}
]
}DELETE /api/v1/merchant/api-keys/:id
Revoke an API key. It will no longer be usable after revocation.
| Parameter | Type | Description |
|---|---|---|
id | string * required | API key ID (path parameter) |
curl -X DELETE https://your-domain.com/api/v1/merchant/api-keys/key_abc123 \
-H "Authorization: Bearer kh_live_abc123"{
"success": true,
"message": "API key revoked"
}EVENTS Webhook Events
Webhooks are sent as POST requests to your configured URL. Each event includes a signature header for verification.
Event Types
| Event | Description |
|---|---|
charge.paid | A charge has been paid successfully |
charge.expired | A charge has expired without payment |
charge.failed | A charge has failed |
{
"id": "evt_abc123",
"type": "charge.paid",
"created_at": "2026-04-19T10:05:32Z",
"data": {
"id": "chg_a1b2c3d4e5f6",
"amount": 10.00,
"currency": "USD",
"status": "paid",
"md5_hash": "e99a18c428cb38d5f260853678922e03",
"bill_number": "BILL-20260419-XXXX",
"paid_at": "2026-04-19T10:05:32Z"
}
}Retry policy: Failed deliveries are retried up to 3 times with exponential backoff (1 min, 5 min, 15 min). Your endpoint should respond within 10 seconds.
SECURITY Verify Signature
Always verify webhook signatures to ensure requests are from KHQR Gateway. The signature is computed using HMAC-SHA256 with your webhook secret.
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Usage in Express:
app.post('/webhooks/khqr', (req, res) => {
const signature = req.headers['x-khqr-signature'];
const payload = JSON.stringify(req.body);
if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(400).send('Invalid signature');
}
// Process event
res.json({ received: true });
});EXAMPLE Node.js
Full Express server with checkout and webhook handling.
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const API_KEY = 'kh_live_abc123';
const BASE_URL = 'https://your-domain.com';
const WEBHOOK_SECRET = 'whsec_abc123def456';
// Create a checkout charge
app.post('/checkout', async (req, res) => {
const { amount, order_id } = req.body;
const response = await fetch(BASE_URL + '/api/v1/charges', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount,
currency: 'USD',
description: 'Order ' + order_id,
metadata: { order_id },
}),
});
const charge = await response.json();
res.json({
qr_image: charge.qr_image,
deep_link: charge.deep_link,
charge_id: charge.id,
});
});
// Check payment status
app.get('/status/:id', async (req, res) => {
const response = await fetch(
BASE_URL + '/api/v1/charges/' + req.params.id,
{ headers: { 'Authorization': 'Bearer ' + API_KEY } }
);
res.json(await response.json());
});
// Webhook endpoint
app.post('/webhooks/khqr', (req, res) => {
const sig = req.headers['x-khqr-signature'];
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (sig !== expected) return res.status(400).send('Invalid');
const event = req.body;
console.log(event.type, event.data.id);
res.json({ received: true });
});
app.listen(3000, () => console.log('Running on :3000'));EXAMPLE Python
Full Flask app with checkout and webhook handling.
import hmac, hashlib, json, requests
from flask import Flask, request, jsonify
app = Flask(__name__)
API_KEY = 'kh_live_abc123'
BASE_URL = 'https://your-domain.com'
WEBHOOK_SECRET = 'whsec_abc123def456'
@app.route('/checkout', methods=['POST'])
def checkout():
data = request.json
resp = requests.post(BASE_URL + '/api/v1/charges', {
'amount': data['amount'],
'currency': 'USD',
'description': 'Order ' + data.get('order_id', ''),
'metadata': {'order_id': data.get('order_id', '')},
}, headers={
'Authorization': 'Bearer ' + API_KEY,
'Content-Type': 'application/json',
})
charge = resp.json()
return jsonify({
'qr_image': charge['qr_image'],
'deep_link': charge['deep_link'],
'charge_id': charge['id'],
})
@app.route('/status/<charge_id>')
def status(charge_id):
resp = requests.get(
BASE_URL + '/api/v1/charges/' + charge_id,
headers={'Authorization': 'Bearer ' + API_KEY},
)
return jsonify(resp.json())
@app.route('/webhooks/khqr', methods=['POST'])
def webhook():
sig = request.headers.get('x-khqr-signature')
payload = json.dumps(request.json, separators=(',', ':'))
expected = hmac.new(
WEBHOOK_SECRET.encode(), payload.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
return jsonify({'error': 'Invalid signature'}), 400
event = request.json
print(event['type'], event['data']['id'])
return jsonify({'received': True})
if __name__ == '__main__':
app.run(port=5000)EXAMPLE PHP
PHP cURL examples for creating charges, checking status, and webhook verification.
<?php
$apiKey = 'kh_live_abc123';
$baseUrl = 'https://your-domain.com';
$webhookSecret = 'whsec_abc123def456';
// Create a charge
function createCharge($amount, $orderId) {
global $apiKey, $baseUrl;
$ch = curl_init($baseUrl . '/api/v1/charges');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => $amount,
'currency' => 'USD',
'description' => 'Order ' . $orderId,
'metadata' => ['order_id' => $orderId],
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
return $response;
}
// Check charge status
function checkCharge($chargeId) {
global $apiKey, $baseUrl;
$ch = curl_init($baseUrl . '/api/v1/charges/' . $chargeId);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
return $response;
}
// Verify webhook signature
function verifyWebhook($payload, $signature) {
global $webhookSecret;
$expected = hash_hmac('sha256', $payload, $webhookSecret);
return hash_equals($expected, $signature);
}
// Webhook handler
$payload = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_KHQR_SIGNATURE'] ?? '';
if (verifyWebhook($payload, $sig)) {
$event = json_decode($payload, true);
error_log($event['type'] . ' ' . $event['data']['id']);
http_response_code(200);
echo json_encode(['received' => true]);
} else {
http_response_code(400);
echo json_encode(['error' => 'Invalid signature']);
}EXAMPLE React
React checkout component with QR display and payment polling.
import { useState, useEffect } from 'react';
const API_KEY = 'kh_live_abc123';
const BASE_URL = 'https://your-domain.com';
export default function Checkout({ amount, orderId }) {
const [charge, setCharge] = useState(null);
const [status, setStatus] = useState('idle');
useEffect(() => {
if (!charge) return;
const interval = setInterval(async () => {
const res = await fetch(
BASE_URL + '/api/v1/charges/' + charge.id,
{ headers: { Authorization: 'Bearer ' + API_KEY } }
);
const data = await res.json();
setStatus(data.status);
if (data.status === 'paid' || data.status === 'expired') {
clearInterval(interval);
}
}, 3000);
return () => clearInterval(interval);
}, [charge]);
const createCharge = async () => {
setStatus('loading');
const res = await fetch(BASE_URL + '/api/v1/charges', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount,
currency: 'USD',
description: 'Order ' + orderId,
metadata: { order_id: orderId },
}),
});
const data = await res.json();
setCharge(data);
setStatus('pending');
};
return (
<div>
{!charge ? (
<button onClick={createCharge}>Pay ${amount}</button>
) : (
<div>
<img src={charge.qr_image} alt="KHQR Code" />
<a href={charge.deep_link}>Pay with Bakong</a>
<p>Status: {status}</p>
</div>
)}
</div>
);
}