# Bongluy > Accept ABA PayWay / KHQR payments in Cambodia from your own code. Create a > payment, render the returned KHQR, and poll until it is paid or expires. > Bongluy never takes custody of funds — money moves directly into the > merchant's own ABA account. Bongluy is an independent service and is not > affiliated with or endorsed by ABA Bank. - Base URL: `https://api.bongluy.com`. Routes sit at the origin root. - Auth: `Authorization: Bearer sk_live_...` — an account-level API key that reaches every store the account owns. Keys expire after 90 days. Keep the key on your server; never ship it to a browser. - Bodies and responses are `application/json`. - Currency: the store's PayWay link must collect in USD. The `currency` field on a payment is metadata only — it does not instruct PayWay or convert funds. - Webhooks do not exist yet. Polling is the supported integration contract. ## Data model One account holds **many stores**, and **each store carries its own ABA PayWay link**. A payment belongs to exactly one store, so the store named on the create call decides which PayWay link — and therefore which ABA account — receives the money. Use one store per destination you collect into: a branch, an outlet, a brand, or a separate business line. There is no limit to plan around. A single API key is account-level and already reaches every store, so routing a payment to a different destination means naming a different store on the request, not holding a second credential. If a system routes money by branch or by seller, model that as one store each rather than rewriting the link on a shared store. ## Identifiers **Do not store Bongluy's `id` or `storeId`.** Integrating requires no new table and no new columns, because every route can be addressed with identifiers the client system already owns: | Your existing identifier | Field you send | Where it is accepted | | --- | --- | --- | | Your store, branch, or outlet id | `merchantStoreId` | Every store-taking route, in place of `storeId` | | Your order, invoice, or sale id | `tranId` | `POST /payment`, `POST /payment/status`, `POST /payment/detail` | Register `merchantStoreId` when the store is created and send a `tranId` on every payment, and the pair addresses that payment for the rest of its life — status, detail, and retries alike. Bongluy remains the system of record for payment state; read it back with the pair rather than mirroring it locally. The UUIDs are still returned, and reading them is fine within a single request. Persisting them is the part that is unnecessary. Two limits are worth knowing before relying on this: a store registered without a `merchantStoreId` can only be addressed by its `storeId`, and `GET /payment` has no `tranId` filter — the exact lookup by `tranId` is `POST /payment/detail`. ## Docs - [API documentation](https://www.bongluy.com/docs/api-integration): endpoints, parameters, response fields, and copy-paste examples in cURL, JavaScript, PHP, and Python. - [Integration prompt](https://www.bongluy.com/llms.txt): this file. ## Integration flow 1. Register a store once with `POST /stores`, binding the ABA PayWay link that receives funds. 2. Create one payment per sale with `POST /payment`. 3. Present checkout: encode `qrString` as a QR yourself, or send the payer to the hosted `checkoutUrl`. On mobile, offer `deeplink.scheme` (iOS) or `deeplink.android`. 4. Poll `POST /payment/status` every 2–3 seconds until `status` leaves `PENDING` or `expireAt` passes. 5. Read `POST /payment/detail` for the durable record used in receipts and reconciliation. ## Endpoints Stores: - `GET /stores` — list every store on the account. No parameters. Returns a bare array, not a paginated envelope. - `POST /stores/detail` — read one store. Body: `storeId` OR `merchantStoreId` (required). Returns the same store object `GET /stores` lists, including a deactivated one — `active` is reported, never used to hide the store. Returns 201. - `POST /stores` — create a store. Body: `name` (string, required, 1–200 chars, unique per account), `paywayLink` (required, an https link on `link.payway.com.kh` that collects in USD — KHR links are rejected), `merchantStoreId` (optional but recommended, your own store id, unique per account — supplying it is what removes any need to store Bongluy's `storeId`), `webhookUrl` and `webhookSecret` (optional, stored but never delivered to). Returns 201. - `POST /stores/update` — partial update. Name the store at the top level and put new values under `changes`. Not a PATCH or PUT. `changes.active: false` blocks new payments while leaving history readable; it is the closest thing to deletion. There is no delete route, and no `GET /stores/:id` — the single-store read is `POST /stores/detail`. Payments: - `POST /payment` — create a payment and reserve its KHQR. Body: `amount` (string, required), `storeId` OR `merchantStoreId` (required), `tranId` (optional, your transaction id), `currency` (optional, defaults to USD). Returns 201. - `GET /payment` — list one store's payments, newest first. Query: `storeId` or `merchantStoreId` (required), `page` (default 1), `per_page` (default 25, max 100, snake_case), `status`. Returns `{ data, page, per_page, total, total_pages }`. - `POST /payment/status` — lightweight state read. Body: a store identifier plus `id` or `tranId`. - `POST /payment/detail` — the full payment object. Same body as the status route. Every store-taking route accepts either `storeId` (Bongluy's UUID) or `merchantStoreId` (your own identifier). Send one or the other; if both are present, `storeId` wins. Every payment-reading route accepts either `id` or `tranId`; if both are present, `id` wins. Prefer your own identifiers in both slots — see Identifiers above. ## The payment object ```json { "id": "8435481a-48a8-4bb2-91d2-bcd1e604fb17", "storeId": "76e0cdea-ab74-455d-b46e-3b9e50f679a2", "status": "PENDING", "amount": "12.50", "currency": "USD", "paywayLink": "https://link.payway.com.kh/aba?id=...", "checkoutUrl": "https://www.bongluy.com/payment/8435481a-48a8-4bb2-91d2-bcd1e604fb17", "expireAt": "2026-08-14T03:14:22.418Z", "settledTranId": null, "receiptUrl": null, "tranId": "INV-1042", "lastError": null, "createdAt": "2026-08-14T03:11:22.418Z", "updatedAt": "2026-08-14T03:11:22.418Z", "settledAt": null, "qrString": "00020101021230...", "deeplink": { "scheme": "abamobilebank://...", "android": "intent://..." } } ``` - `qrString` is the KHQR payload and is authoritative. `deeplink` is null whenever `qrString` is null, and is derived from an undocumented upstream client that can change without notice — always keep the QR visible. - `settledTranId`, `receiptUrl`, and `settledAt` stay null until `SUCCESS`. `settledTranId` is ABA's id for the completed transaction, which is not the same thing as your `tranId`. - `paywayLink` is snapshotted at creation; later edits to the store do not rewrite existing payments. Status is one of `PENDING`, `SUCCESS`, `EXPIRED`, `FAILED`. `SUCCESS` is the only status that means money moved. ## Money `amount` is a **string**, not a number, because decimal money must not pass through JSON floating point. It must match `^\d+(\.\d{1,2})?$` — zero, one, or two fractional digits. The API currently accepts `"0"` and `"0.00"`, so enforce your own minimum before calling it. ## Idempotency `tranId` is unique within a store and acts as the idempotency key. Repeating a create request with an existing `tranId` returns the original payment verbatim, with no second upstream call — so a timed-out create is safe to retry. Payments created without a `tranId` do not collide, but a lost create response cannot then be recovered through your own identifier, which leaves holding Bongluy's UUID as the only way to find the payment again. Always send a `tranId` instead. ## Expiry ABA supplies the lifetime; Bongluy falls back to 180 seconds only when ABA omits it. Read `expireAt` from the response instead of hardcoding three minutes. If upstream polling exhausts its retries a payment can still read `PENDING` after `expireAt` — treat that as unpaid, stop polling, and reconcile later rather than leaving the payer on a spinner. ## The status payload is not the payment object `POST /payment/status` returns `{ paymentId, status, expireAt, at, settledTranId, receipt }`. Note the differences: the id is `paymentId`, the receipt is `receipt` rather than `receiptUrl`, and `expireAt` and `at` are epoch **milliseconds** rather than ISO strings. While pending, `settledTranId` and `receipt` are absent, not null. ## Errors ```json { "statusCode": 400, "message": ["amount must be a positive decimal"], "error": "Bad Request" } ``` `message` is a string for most failures but an **array of strings** for validation errors — branch on the type before displaying it. `error` is the HTTP reason phrase, not a stable machine-readable code. - `400` validation failure, or a store update with no recognized changes. - `401` missing, invalid, expired, revoked, or under-scoped API key. All key failures collapse to 401, so check the key's 90-day age before debugging request code. - `403` unrecognized origin on a public route. - `404` missing or foreign store/payment. Foreign resources are deliberately indistinguishable from missing ones. - `409` `merchantStoreId` already in use. - `429` rate limit exceeded. There is no `Retry-After` header; back off for the rest of the minute. - `500` PayWay failure, server failure, or currently a duplicate store name. Retry with the same `tranId` so a request that actually succeeded upstream cannot create a duplicate. ## Rate limits - 600 requests per minute, per API key. - 20 store writes per minute, per account. - Hosted checkout: 20 requests per second per payment, 200 per second overall. At 2.5-second intervals a single three-minute checkout costs about 72 calls, so roughly eight concurrent checkouts saturate one key. Poll once per payment on your server, not from every open browser tab. ## Optional - [Privacy Policy](https://www.bongluy.com/privacy) - [Terms of Service](https://www.bongluy.com/terms)