QRPAY
Home Features Pricing API Docs

API Reference

APIKHQR REST API · v1

Base URL https://www.khqrapi.com/api/v1

Authentication

Include your API key in the X-API-Key header on every request. Get your key from the dashboard.

Header
X-API-Key: sk_live_your_api_key_here

Security: Never expose your key in front-end or mobile code. Always call the API from your server/backend.

Error Codes

CodeMeaning
400 Bad Request — invalid parameters
401 Unauthorized — missing or invalid API key
404 Not Found — transaction not found
409 Conflict — payment already claimed
422 Unprocessable — merchant not configured
429 Too Many Requests — rate limit
500 Server Error — contact support
POST

/api/v1/khqr/generate

Generate a KHQR payment QR code. Returns qr_string, a ready-to-use qr_image_url (PNG — use directly in <img src>), and md5 for polling.

Request Body (JSON)

FieldTypeDescription
amountnumberRequiredAmount (e.g. 5.00)
currencystringRequiredUSD or KHR
notestringOptionalPayment description (max 200)

Example Request

curl -X POST https://www.khqrapi.com/api/v1/khqr/generate \
  -H "X-API-Key: sk_live_..." -H "Content-Type: application/json" \
  -d '{"amount":5.00,"currency":"USD","note":"Order #1234"}'
resp = requests.post(
    "https://www.khqrapi.com/api/v1/khqr/generate",
    json={"amount": 5.00, "currency": "USD", "note": "Order #1234"},
    headers={"X-API-Key": "sk_live_..."},
)
data = resp.json()
print(data["qr_image_url"])  # <-- use in <img src> directly
print(data["md5"])            # <-- use to poll /status
const res = await fetch("https://www.khqrapi.com/api/v1/khqr/generate", {
  method: "POST",
  headers: { "X-API-Key": "sk_live_...", "Content-Type": "application/json" },
  body: JSON.stringify({ amount: 5.00, currency: "USD", note: "Order #1234" }),
});
const { qr_image_url, md5 } = await res.json();
// Display QR:
document.getElementById("qr").src = qr_image_url;
$ch = curl_init("https://www.khqrapi.com/api/v1/khqr/generate");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true,
    CURLOPT_HTTPHEADER  => ["X-API-Key: sk_live_...", "Content-Type: application/json"],
    CURLOPT_POSTFIELDS  => json_encode(["amount"=>5.00,"currency"=>"USD","note"=>"Order #1234"]),
]);
$data = json_decode(curl_exec($ch), true);
// ⚠️ Call your backend proxy — never embed key in app
final res = await http.post(
  Uri.parse('$backendUrl/generate-qr'),
  headers: {'Content-Type': 'application/json'},
  body: jsonEncode({'amount': 5.00, 'currency': 'USD', 'note': 'Order #1234'}),
);
final data = jsonDecode(res.body);
// Show QR from hosted image URL (no library needed):
Image.network(data['qr_image_url']);

Response 201

JSON
{
  "success": true,
  "bill_number": "PAY260524123456789",
  "qr_string": "000201010212...",
  "qr_image_url": "https://yourdomain.com/qr/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "md5": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "amount": 5.00,
  "currency": "USD",
  "expires_at": "2026-05-24T12:35:00+07:00"
}
FieldTypeDescription
bill_numberstringUnique bill reference
qr_stringstringRaw KHQR EMVCo string — pass to a QR renderer library
qr_image_urlstringReady-to-use 300×300 PNG URL — use directly as <img src="...">, no extra library needed
md5stringMD5 hash — pass to /status?md5= to poll payment
amountnumberConfirmed amount
currencystringUSD or KHR
expires_atISO 8601QR expiry timestamp (5 min after generation)

Sign in to test the API live

Sign in →
GET

/api/v1/khqr/status

Poll for payment status using the md5 from generate. Returns WAITING, PAID, or EXPIRED.

Query Parameters

ParamTypeDescription
md5stringRequiredMD5 from generate response
curl "https://www.khqrapi.com/api/v1/khqr/status?md5=a1b2c3d4..." \
  -H "X-API-Key: sk_live_..."
data = requests.get(
    "https://www.khqrapi.com/api/v1/khqr/status",
    params={"md5": md5},
    headers={"X-API-Key": "sk_live_..."},
).json()
print(data["status"])  # WAITING | PAID | EXPIRED
const res = await fetch(`https://www.khqrapi.com/api/v1/khqr/status?md5=${md5}`, {
  headers: { "X-API-Key": "sk_live_..." },
});
const { status } = await res.json(); // WAITING | PAID | EXPIRED
$data = json_decode(file_get_contents(
    "https://www.khqrapi.com/api/v1/khqr/status?md5=" . urlencode($md5), false,
    stream_context_create(['http' => ['header' => 'X-API-Key: sk_live_...']]),
), true);
// $data['status'] → 'WAITING' | 'PAID' | 'EXPIRED'
final res = await http.get(
  Uri.parse('$backendUrl/payment-status?md5=$md5'),
);
final data = jsonDecode(res.body);
// data['status'] → 'WAITING' | 'PAID' | 'EXPIRED'

Response 200

{
  "success": true,
  "status": "PAID",
  "bill_number": "PAY260524123456789",
  "md5": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "amount": 5.00,
  "currency": "USD",
  "paid_at": "2026-05-24T12:31:42+07:00",
  "expires_at": "2026-05-24T12:35:00+07:00"
}

Tip: Poll every 3–5 seconds until PAID or EXPIRED. Each md5 can only be claimed once.

Sign in to test the API live

Sign in →

🐍 Python

Requires: pip install requests

Python 3.8+
import requests, time

API_KEY = "sk_live_your_api_key_here"
BASE    = "https://www.khqrapi.com/api/v1"
H       = {"X-API-Key": API_KEY}

def generate(amount, currency="USD", note=""):
    r = requests.post(f"{BASE}/khqr/generate",
                      json={"amount": amount, "currency": currency, "note": note}, headers=H)
    r.raise_for_status()
    return r.json()

def wait_paid(md5, timeout=300):
    until = time.time() + timeout
    while time.time() < until:
        data = requests.get(f"{BASE}/khqr/status", params={"md5": md5}, headers=H).json()
        if data["status"] in ("PAID", "EXPIRED"):
            return data
        time.sleep(5)

# ── Usage ──────────────────────────────────────────────
qr = generate(5.00, "USD", "Order #1234")
print("QR image:", qr["qr_image_url"]) # use in  directly
print("QR string:", qr["qr_string"])   # raw EMVCo string (optional)

result = wait_paid(qr["md5"])
print("Status:", result["status"])    # PAID or EXPIRED

⚡ JavaScript / Node.js

Node.js 18+ (built-in fetch) or any backend JS runtime

ES2020+ / Node.js 18+
const API_KEY = "sk_live_your_api_key_here";
const BASE    = "https://www.khqrapi.com/api/v1";
const H       = { "X-API-Key": API_KEY, "Content-Type": "application/json" };

async function generate(amount, currency = "USD", note = "") {
  const res = await fetch(`${BASE}/khqr/generate`, {
    method: "POST", headers: H,
    body: JSON.stringify({ amount, currency, note }),
  });
  return res.json();
}

async function waitPaid(md5, timeout = 300_000) {
  const until = Date.now() + timeout;
  while (Date.now() < until) {
    const { status } = await fetch(`${BASE}/khqr/status?md5=${md5}`, { headers: H }).then(r => r.json());
    if (status === "PAID" || status === "EXPIRED") return status;
    await new Promise(r => setTimeout(r, 5000));
  }
}

// ── Usage ──────────────────────────────────────────────
const qr = await generate(5.00, "USD", "Order #1234");
console.log("QR image:", qr.qr_image_url); // use in  directly
document.getElementById("qr").src = qr.qr_image_url;

const status = await waitPaid(qr.md5);
console.log("Status:", status);        // PAID or EXPIRED

🐘 PHP

PHP 8.0+ with ext-curl

PHP 8.0+
<?php
const API_KEY = 'sk_live_your_api_key_here';
const BASE    = 'https://www.khqrapi.com/api/v1';

function api(string $m, string $path, array $body = []): array {
    $ch = curl_init(BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $m,
        CURLOPT_HTTPHEADER  => ['X-API-Key: ' . API_KEY, 'Content-Type: application/json'],
        CURLOPT_POSTFIELDS  => $body ? json_encode($body) : null,
    ]);
    return json_decode(curl_exec($ch), true);
}

function generate(float $amount, string $currency = 'USD', string $note = ''): array {
    return api('POST', '/khqr/generate', compact('amount', 'currency', 'note'));
}

function waitPaid(string $md5, int $timeout = 300): ?array {
    $until = time() + $timeout;
    while (time() < $until) {
        $data = api('GET', '/khqr/status?md5=' . urlencode($md5));
        if (in_array($data['status'], ['PAID', 'EXPIRED'])) return $data;
        sleep(5);
    }
    return null;
}

// ── Usage ──────────────────────────────────────────────
$qr = generate(5.00, 'USD', 'Order #1234');
echo "QR image: {$qr['qr_image_url']}\n"; // use in  directly
echo "QR string: {$qr['qr_string']}\n";

$result = waitPaid($qr['md5']);
echo "Status: {$result['status']}\n"; // PAID or EXPIRED

💙 Flutter / Dart

Add http: ^1.2.0 to pubspec.yaml

Important: Never embed your API key in Flutter/mobile apps. Route all API calls through your own backend server.

1. Flutter app calls your backend:

Dart / Flutter
import 'dart:convert';
import 'package:http/http.dart' as http;

const backendUrl = 'https://your-server.com'; // your backend, NOT apikhqr directly

/// Generate QR via your backend proxy.
Future<Map<String, dynamic>> generateQR(double amount, {String currency = 'USD', String note = ''}) async {
  final res = await http.post(
    Uri.parse('$backendUrl/generate-qr'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({'amount': amount, 'currency': currency, 'note': note}),
  );
  return jsonDecode(res.body) as Map<String, dynamic>;
}

/// Poll for payment status via your backend.
Future<String> waitForPayment(String md5) async {
  while (true) {
    final res  = await http.get(Uri.parse('$backendUrl/payment-status?md5=$md5'));
    final data = jsonDecode(res.body) as Map<String, dynamic>;
    if (data['status'] == 'PAID' || data['status'] == 'EXPIRED') return data['status'];
    await Future.delayed(const Duration(seconds: 5));
  }
}

// Usage:
// final qr     = await generateQR(5.00, currency: 'USD', note: 'Order #1234');
// final qrImageUrl = qr['qr_image_url'];  // ← use Image.network() directly, no library!
// final qrStr      = qr['qr_string'];       // raw string if you prefer qr_flutter
// final status = await waitForPayment(qr['md5']); // 'PAID' or 'EXPIRED'

2. Your backend proxy (Node.js/Express):

Node.js / Express backend
import express from "express";
const app = express();
app.use(express.json());

const API_KEY = process.env.APIKHQR_KEY; // store in .env, NEVER in the app
const BASE    = "https://www.khqrapi.com/api/v1";

app.post("/generate-qr", async (req, res) => {
  const r = await fetch(`${BASE}/khqr/generate`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify(req.body),
  });
  res.json(await r.json());
});

app.get("/payment-status", async (req, res) => {
  const r = await fetch(`${BASE}/khqr/status?md5=${req.query.md5}`, {
    headers: { "X-API-Key": API_KEY },
  });
  res.json(await r.json());
});

app.listen(3000);

Ready to integrate?

Sign up free to get your API key and start accepting KHQR payments.

Get Started Free →