TopUpKH

API Documentation

Accept ABA KHQR payments on your own website through a simple REST API — real QR codes, a hosted checkout page, and signed webhooks. Payments settle directly into your own ABA account.

Base URL: https://topupkh.store
Before you start — 2 things to know

1. You need your own ABA Payment Link. This comes from your own ABA Bank Business account, via the ABA Mobile Business app — not from TopUpKH, and we can't create one for you. Money paid through it goes directly into your own ABA account. If you don't have one yet, get it from ABA Bank first, then paste it into your Profile page.

2. You don't need to build a payment system. TopUpKH handles the checkout page, QR code, and payment status for you. The simplest option needs zero code — send customers to the link we give you and you're done. Want your own website to detect "paid" automatically? That needs one small piece of code from a developer (see Webhooks below) — not a full payment system.

1. Getting Started

  1. Go to topupkh.store/signup
  2. Enter your business name, your own ABA PayWay Payment Link, and (optionally) a phone number
  3. You'll immediately receive an API Key and API Secret
Save both immediately. The API Secret is shown once and cannot be recovered — only regenerated, which invalidates the old one. Your API Key can be viewed and copied anytime from your merchant dashboard.
CredentialUsed for
API KeyAuthenticates every API call
API SecretVerifies webhook signatures, and logs you into your dashboard

Keep both server-side only — never expose them in browser JavaScript, mobile app code, or a public repository.

2. Authentication

Every API request (except signup) requires your API Key in the Authorization header:

header
Authorization: Bearer kg_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

x-api-key: kg_live_... also works as an alternative header.

3. Generate a Payment

Call this the moment a customer is ready to pay.

GET/generate-payment

Query parameters

ParameterRequiredDescription
amountYesDecimal USD amount, up to 2 decimals (e.g. 5.00)
referenceYesA unique ID for this payment — your order ID works well
payment_linkNoOverrides your default ABA PayWay link for this one payment
success_urlNoWhere the customer is redirected after payment confirms
webhook_urlNoYour endpoint to receive a signed payment notification

Example request

bash
curl -G https://topupkh.store/generate-payment \
  -H "Authorization: Bearer kg_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  --data-urlencode "amount=5.00" \
  --data-urlencode "reference=ORDER-1234" \
  --data-urlencode "success_url=https://yourshop.com/thank-you" \
  --data-urlencode "webhook_url=https://yourshop.com/api/khqr-webhook"

Example response

json
{
  "status": "success",
  "reference": "ORDER-1234",
  "checkout_url": "https://topupkh.store/aba_khqr/checkout/ORDER-1234",
  "qr_string": "00020101021230510016abaakhppxxx...",
  "deeplink": "abamobilebank://ababank.com?type=payway&qrcode=...",
  "amount": "5.00",
  "currency": "USD",
  "expire_in_sec": 180
}

Error responses

HTTPCause
400Missing amount/reference, or amount isn't a valid decimal
401Missing or invalid API key
409reference already exists — use a new one
502Could not generate the KHQR code (ABA-side issue)

4. Send the Customer to Pay

Option A — Hosted checkout (recommended)

Redirect the customer to checkout_url from the response above. It shows a branded page with the QR code, a live countdown, and an "Open in ABA Mobile" button, and automatically updates to Success or Expired. Zero frontend work required.

Option B — Embed it yourself

Render your own QR image from qr_string, and link a button to deeplink for one-tap payment on mobile. Use this if you want the payment step to stay visually inside your own site.

5. Webhooks

If you passed webhook_url, we POST this the moment the payment resolves:

json
{
  "status": "success",
  "reference": "ORDER-1234",
  "aba_tran_id": "178440319630275",
  "receipt_url": "https://pwapp.ababank.com/.../download-receipt?...",
  "bank_reference": null,
  "amount": "5.00",
  "currency": "USD",
  "payment_method": "ABA Pay & KHQR",
  "created_at": "2026-07-19T04:20:00.000Z",
  "paid_at": "2026-07-19T04:22:14.000Z"
}

status is "success" or "expire".

Headers sent with every webhook

headers
X-KHQR-Timestamp: 1784403196
X-KHQR-Signature: sha256=<hmac-hex>
Always verify the signature before trusting the payload — this proves it genuinely came from TopUpKH and wasn't forged by someone who guessed your reference.

Verify in Node.js

javascript
const crypto = require('crypto');

function isValidWebhook(rawBody, headers, apiSecret) {
  const ts = headers['x-khqr-timestamp'];
  const expected = 'sha256=' + crypto
    .createHmac('sha256', apiSecret)
    .update(`${ts}.${rawBody}`)
    .digest('hex');
  return headers['x-khqr-signature'] === expected;
}

Verify in PHP

php
<?php
function isValidWebhook($rawBody, $timestamp, $signatureHeader, $apiSecret) {
    $expected = 'sha256=' . hash_hmac('sha256', "$timestamp.$rawBody", $apiSecret);
    return hash_equals($expected, $signatureHeader);
}

$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_KHQR_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_KHQR_SIGNATURE'] ?? '';

if (!isValidWebhook($rawBody, $timestamp, $signature, $yourApiSecret)) {
    http_response_code(400);
    exit('invalid signature');
}

$payment = json_decode($rawBody, true);
if ($payment['status'] === 'success') {
    // mark $payment['reference'] as paid in your own database
}

If your endpoint doesn't return a 2xx response, delivery is retried up to 5 times with exponential backoff.

6. Polling (alternative to webhooks)

GET/api/checkout/status/:reference
bash
curl https://topupkh.store/api/checkout/status/ORDER-1234 \
  -H "Authorization: Bearer kg_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "reference": "ORDER-1234",
  "status": "pending",
  "amount": "5.00",
  "currency": "USD",
  "aba_tran_id": null,
  "receipt_url": null,
  "bank_reference": null
}

status is one of: pending, success, expire.

7. Quick Integration Examples

Node.js / Express

javascript
const fetch = require('node-fetch');

app.post('/checkout', async (req, res) => {
  const params = new URLSearchParams({
    amount: req.body.total.toFixed(2),
    reference: `ORDER-${req.body.orderId}`,
    success_url: 'https://yourshop.com/thank-you',
    webhook_url: 'https://yourshop.com/api/khqr-webhook',
  });

  const r = await fetch(`https://topupkh.store/generate-payment?${params}`, {
    headers: { Authorization: `Bearer ${process.env.TOPUPKH_API_KEY}` },
  });
  const payment = await r.json();
  res.redirect(payment.checkout_url);
});

PHP

php
<?php
$params = http_build_query([
    'amount' => number_format($total, 2, '.', ''),
    'reference' => 'ORDER-' . $orderId,
    'success_url' => 'https://yourshop.com/thank-you',
    'webhook_url' => 'https://yourshop.com/api/khqr-webhook',
]);

$ch = curl_init("https://topupkh.store/generate-payment?$params");
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . TOPUPKH_API_KEY]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$payment = json_decode(curl_exec($ch), true);
curl_close($ch);

header('Location: ' . $payment['checkout_url']);
exit;

8. Testing Before Going Live

There's no separate sandbox — every request generates a real KHQR code against your real ABA PayWay link. To test safely:

  • Use a small real amount, e.g. amount=0.01
  • Scan it yourself and confirm the webhook/status flow works end to end before wiring up real customer traffic

9. All Endpoints

EndpointMethodAuthPurpose
/signupPOSTNoneCreate a merchant account
/generate-paymentGETAPI KeyCreate a new KHQR payment
/api/checkout/status/:referenceGETAPI KeyCheck a payment's status
/aba_khqr/checkout/:referenceGETNoneHosted checkout page — send customers here