One API for ABA PayWay and Bakong KHQR. Create a payment, render the returned QR, and observe the terminal result by webhook or polling.
| Base URL | https://aba-api.annoymous.com — routes sit at the origin root |
|---|---|
| Authenticated routes | Authorization: Bearer sk_live_… |
| Public checkout routes | x-checkout-key: … — server-side only, never in browser code |
Keys belong to the account, not to a single store, so one key reaches every store you own. Keys expire after 90 days and are rate limited to 600 requests per minute. The older store-scoped key still works — it resolves to the owning account.
| Method | Path | What it does |
|---|---|---|
| GET | /stores | List the account's stores |
| POST | /stores/detail | Read one store |
| POST | /stores | Create a store and configure its rails |
| POST | /stores/update | Update or deactivate a store |
| POST | /payment | Create a payment and reserve its KHQR |
| GET | /payment?storeId=…&page=1 | List and page a store's payments |
| POST | /payment/status | Poll a payment by id or by your tranId |
| POST | /payment/detail | Read the full durable payment record |
| GET | /public/payment/:id | Payer-safe payment object |
| GET | /public/payment/:id/status | Current status and clocks |
| POST | /public/payment/:id/retry | Re-check status and reopen ABA polling |
curl -X POST https://aba-api.annoymous.com/stores \
-H 'authorization: Bearer sk_live_…' \
-H 'content-type: application/json' \
-d '{"name":"Tykea Coffee","merchantStoreId":"branch-2",
"paywayLink":"https://link.payway.com.kh/aba?id=usd…",
"khrPaywayLink":"https://link.payway.com.kh/aba?id=khr…",
"bakongAccountId":"your_name@aclb"}'
paywayLink is required. khrPaywayLink and bakongAccountId enable the other rails. merchantStoreId is your own identifier — registering it means you never have to store our ids.
curl -X POST https://aba-api.annoymous.com/payment \
-H 'authorization: Bearer sk_live_…' \
-H 'content-type: application/json' \
-d '{"merchantStoreId":"branch-2","amount":"12.50",
"currency":"USD","provider":"BAKONG","tranId":"INV-1042"}'
{
"id": "8435481a-48a8-4bb2-91d2-bcd1e604fb17",
"status": "PENDING",
"amount": "12.50",
"currency": "USD",
"tranId": "INV-1042",
"qrString": "00020101021230…",
"khqrMd5": "e0f3d55c4ca178f4d2c45e99d4d9d7f8",
"paywayLink": null,
"deeplink": null,
"checkoutUrl": "https://aba-api.annoymous.com/payment/8435481a-…",
"expireAt": "2026-08-14T03:14:22.418Z"
}
qrString is a plain KHQR string — draw it anywhere: a web checkout, a POS screen, a printed slip. Or send the payer straight to checkoutUrl, which is the hosted checkout page.
Keep the QR visible. deeplink is derived from ABA's checkout client and is undocumented upstream, so it can change without notice. qrString is the source of truth.
curl -X POST https://aba-api.annoymous.com/payment/status \
-H 'authorization: Bearer sk_live_…' \
-H 'content-type: application/json' \
-d '{"merchantStoreId":"branch-2","tranId":"INV-1042"}'
{
"paymentId": "8435481a-…",
"status": "SUCCESS",
"expireAt": 1786763662418,
"settledTranId": "1234567890",
"receipt": "https://…",
"at": 1786763501992
}
Poll every two or three seconds until status leaves PENDING. This payload is not the payment object: expireAt and at are epoch milliseconds, and the receipt field is named receipt. While pending, settledTranId and receipt are absent, not null.
The end of the automatic check window is not the end of the payment. On ABA the QR stays payable for 24 hours while the automatic checks only run for the first few minutes, so a status read taken after that window reopens the checks and a payment made later is still discovered. On Bakong the QR really does die at its deadline and the provider is checked one last time before EXPIRED is written; if Bakong cannot answer the payment stays PENDING, because a local clock never manufactures an EXPIRED status.
A deactivated store still owns its payment history: active: false blocks new payments, but GET /payment, POST /payment/status and POST /payment/detail keep serving the payments that store already owns.
| Provider | Currency | Store requirement | Amount rule |
|---|---|---|---|
| ABA | USD | paywayLink | at least 0.01 |
| ABA | KHR | khrPaywayLink | at least 100 |
| BAKONG | USD | bakongAccountId | at least 0.01 |
| BAKONG | KHR | bakongAccountId | whole riel, at least 100 |
amount is a string and must match ^\d+(\.\d{1,2})?$. No conversion occurs.
tranId is unique within a store. Repeating a create with an existing tranId returns the original payment verbatim, without a second upstream call — a timed-out create is always safe to retry. The original response wins even if the retry changes the amount, currency or provider, so treat tranId as the identity of one payment intent.
A repeat returns 200 with the original payment; the first call returns 201.
Set webhookUrl and webhookSecret on the store to receive one POST when a payment reaches SUCCESS or EXPIRED. A PENDING payment produces nothing.
curl -X POST https://aba-api.annoymous.com/stores/update \
-H 'authorization: Bearer sk_live_…' \
-H 'content-type: application/json' \
-d '{"merchantStoreId":"branch-2","changes":{
"webhookUrl":"https://yourshop.com/hooks/bongluy",
"webhookSecret":"whsec_…"}}'
X-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request bytes, keyed by the store's secret. Do not hash re-serialised JSON. Return any 2xx within 10 seconds and fulfil afterwards; everything else is retried 10 times over about 17 minutes.
{ "statusCode": 400, "message": "…", "error": "Bad Request" }
For validation failures message is an array of field complaints — branch on the type before displaying it.
| Status | Meaning |
|---|---|
| 400 | Validation, missing rail configuration, fractional Bakong KHR, or no recognised store changes |
| 401 | Missing, invalid, expired, revoked or under-scoped API key |
| 403 | Unrecognised origin on a public route |
| 404 | Missing or foreign store/payment |
| 409 | merchantStoreId already in use |
| 429 | Rate limited |
| 500 | Payment provider or server failure — retry with the same tranId |
| Authenticated API | 600 requests per minute per API key |
|---|---|
| Store writes | 20 per minute per account |
| Hosted checkout | 20 requests per second per payment, 200 per second overall |
A local timeout is not an EXPIRED status. Do not release goods on a timeout, but do not ask the payer to pay twice either — reconcile until the server reports a terminal state.