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.

Header Format
Authorization: Bearer kh_live_your_api_key_here
ParameterValueDescription
AuthorizationBearer {api_key}Your API key prefixed with "Bearer "

BASE Base URL

All API requests should be made to this base URL.

Base URL
https://your-domain.com

POST /api/v1/charges

Create a new KHQR payment charge. Returns a QR code, deep link, and payment details.

ParameterTypeDescription
amountnumber * requiredPayment amount in USD (minimum $0.01)
currencystring optionalCurrency code (default: "USD")
descriptionstring optionalDescription of the payment
metadataobject optionalCustom key-value metadata
Request · curl
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"
    }
  }'
Response · 200 OK
{
  "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.

ParameterTypeDescription
idstring * requiredCharge ID (path parameter)

Possible status values:pendingpaidexpiredfailed

Request · curl
curl https://your-domain.com/api/v1/charges/chg_a1b2c3d4e5f6 \
  -H "Authorization: Bearer kh_live_abc123"
Response · 200 OK
{
  "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.

ParameterTypeDescription
idstring * requiredCharge ID (path parameter)
Request · curl
curl -X POST https://your-domain.com/api/v1/charges/chg_a1b2c3d4e5f6/check \
  -H "Authorization: Bearer kh_live_abc123"
Response · 200 OK
{
  "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.

ParameterTypeDescription
statusstring optionalFilter by status: pending, paid, expired, failed
limitnumber optionalResults per page (default: 20, max: 100)
offsetnumber optionalOffset for pagination (default: 0)
Request · curl
curl "https://your-domain.com/api/v1/charges?status=paid&limit=10&offset=0" \
  -H "Authorization: Bearer kh_live_abc123"
Response · 200 OK
{
  "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.

ParameterTypeDescription
emailstring * requiredMerchant email address
namestring * requiredMerchant display name
Request · curl
curl -X POST https://your-domain.com/api/v1/merchant/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "merchant@example.com",
    "name": "My Store"
  }'
Response · 200 OK
{
  "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.

ParameterTypeDescription
urlstring * requiredHTTPS URL to receive webhook events
Request · curl
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"
  }'
Response · 200 OK
{
  "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.

Request · curl
curl -X POST https://your-domain.com/api/v1/merchant/webhook/test \
  -H "Authorization: Bearer kh_live_abc123"
Response · 200 OK
{
  "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.

Request · curl
curl https://your-domain.com/api/v1/merchant/stats \
  -H "Authorization: Bearer kh_live_abc123"
Response · 200 OK
{
  "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.

ParameterTypeDescription
namestring * requiredA descriptive name for the key
environmentstring optional"live" or "test" (default: "live")
Request · curl
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"
  }'
Response · 200 OK
{
  "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.

Request · curl
curl https://your-domain.com/api/v1/merchant/api-keys \
  -H "Authorization: Bearer kh_live_abc123"
Response · 200 OK
{
  "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.

ParameterTypeDescription
idstring * requiredAPI key ID (path parameter)
Request · curl
curl -X DELETE https://your-domain.com/api/v1/merchant/api-keys/key_abc123 \
  -H "Authorization: Bearer kh_live_abc123"
Response · 200 OK
{
  "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

EventDescription
charge.paidA charge has been paid successfully
charge.expiredA charge has expired without payment
charge.failedA charge has failed
Webhook Payload · POST
{
  "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.

Node.js · Verification
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.

Node.js · Express + KHQR
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.

Python · Flask + KHQR
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 · cURL + KHQR
<?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.

React · Checkout Component
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>
  );
}