Authentication stays on your server.
Authenticated store and payment routes accept an account-level API key.
- Credential
Authorization: Bearer sk_live_...- Held by
- Your server
- Access
- Stores and payments
| Credential | Held by | Access |
|---|---|---|
Authorization: Bearer sk_live_... | Your server | Stores and payments |
Obtain a key
Log in, open the API keys page, create a key, and copy it.
Copy this key now
The app shows a new key only once.
sk_live_...Store the copied value in your server's .env file, for example BONGLUY_API_KEY=sk_live_.... Keep it on the server and never expose it in browser code.
Key facts worth knowing up front
- Scope
- Keys belong to the account, not to one store. A key reaches every store the account owns.
- Lifetime
- 90 days. Rotate before the deadline; an expired key fails as 401, the same as an invalid one.
- Name
- Up to 32 characters. It is a label for you and carries no meaning to the API.
- Recovery
- None. Only the hash is kept, so a lost key must be replaced rather than looked up.
One account, as many stores as you need.
An account can hold any number of stores, and each store carries its own ABA PayWay link. A payment belongs to exactly one store, so the store you name on a 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. Registering several is normal, and there is no limit to plan around. Your single API key already reaches all of them, so switching destinations is a matter of naming a different store on the request, not of holding a second credential.
storeIdBongluy's UUID for the store.
merchantStoreIdYour existing identifier, such as branch-2 or SHOP-KH-01.
Send one identifier or the other. A request with neither fails validation; if both are present, storeId wins. Register merchantStoreId when you create the store and you never have to store Bongluy'sstoreId at all — your own id addresses the store everywhere it is accepted.
Omitting merchantStoreId is valid, and stores without one do not collide with each other. Those stores can only be addressed with Bongluy's storeId, which is the mapping supplying your own id avoids.
{
"id": "76e0cdea-ab74-455d-b46e-3b9e50f679a2",
"userId": "c1f0...",
"name": "Tykea Coffee",
"merchantStoreId": "branch-2",
"paywayLink": "https://link.payway.com.kh/aba?id=...",
"webhookUrl": null,
"active": true,
"createdAt": "2026-08-14T03:11:22.418Z",
"updatedAt": "2026-08-14T03:11:22.418Z"
}- Field
id- Type
- string
- Meaning
- Bongluy's UUID for the store. Accepted as storeId everywhere else.
- Field
userId- Type
- string
- Meaning
- The account that owns the store. Taken from your credential at creation.
- Field
name- Type
- string
- Meaning
- The display name you chose. Unique within the account.
- Field
merchantStoreId- Type
- string | null
- Meaning
- Your own identifier, or null if you did not supply one.
- Field
paywayLink- Type
- string
- Meaning
- The ABA PayWay link that receives funds for this store.
- Field
webhookUrl- Type
- string | null
- Meaning
- Stored for future use. Nothing is delivered to it today.
- Field
active- Type
- boolean
- Meaning
- False blocks new payments. Existing payments stay readable.
- Field
createdAt- Type
- string
- Meaning
- ISO 8601 timestamp.
- Field
updatedAt- Type
- string
- Meaning
- ISO 8601 timestamp of the last write.
| Field | Type | Meaning |
|---|---|---|
id | string | Bongluy's UUID for the store. Accepted as storeId everywhere else. |
userId | string | The account that owns the store. Taken from your credential at creation. |
name | string | The display name you chose. Unique within the account. |
merchantStoreId | string | null | Your own identifier, or null if you did not supply one. |
paywayLink | string | The ABA PayWay link that receives funds for this store. |
webhookUrl | string | null | Stored for future use. Nothing is delivered to it today. |
active | boolean | False blocks new payments. Existing payments stay readable. |
createdAt | string | ISO 8601 timestamp. |
updatedAt | string | ISO 8601 timestamp of the last write. |
webhookSecret is accepted on writes but never returned.
/storesList every store owned by the account.
scope store:readTakes no parameters. The response is a bare array of store objects — not a paginated envelope. There is no filtering, and ordering is not guaranteed.
curl https://api.bongluy.com/stores \
-H 'authorization: Bearer sk_live_...'/stores/detail201Read one store by either identifier.
scope store:readName the store in the body and the response is the same store object GET /stores returns for it. It is a POST because the identifier travels in the body; there is noGET /stores/:id.
- Field
storeId- Type
- string
- Required
- Either
- Notes
- Bongluy's UUID for the store
- Field
merchantStoreId- Type
- string
- Required
- Either
- Notes
- Your own identifier. Ignored if storeId is also present
| Field | Type | Required | Notes |
|---|---|---|---|
storeId | string | Either | Bongluy's UUID for the store |
merchantStoreId | string | Either | Your own identifier. Ignored if storeId is also present |
curl -X POST https://api.bongluy.com/stores/detail \
-H 'authorization: Bearer sk_live_...' \
-H 'content-type: application/json' \
-d '{"merchantStoreId":"branch-2"}'Missing and foreign stores both return404 "Store not found", for the same reason as on update: a 403 would confirm that the id exists.
A deactivated store is still readable.active is reported here, not enforced, so turning a store off never hides it.
/stores201Create a store and bind its PayWay link.
scope store:create- Field
name- Type
- string
- Required
- Yes
- Notes
- 1-200 characters; unique within the account
- Field
paywayLink- Type
- URL string
- Required
- Yes
- Notes
- An https link on link.payway.com.kh. The link must collect in USD; KHR links are rejected
- Field
merchantStoreId- Type
- string
- Required
- No
- Notes
- 1-200 characters; unique within the account
- Field
webhookUrl- Type
- URL string
- Required
- No
- Notes
- Stored but not delivered to
- Field
webhookSecret- Type
- string
- Required
- No
- Notes
- Stored and never returned
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | 1-200 characters; unique within the account |
paywayLink | URL string | Yes | An https link on link.payway.com.kh. The link must collect in USD; KHR links are rejected |
merchantStoreId | string | No | 1-200 characters; unique within the account |
webhookUrl | URL string | No | Stored but not delivered to |
webhookSecret | string | No | Stored and never returned |
curl -X POST https://api.bongluy.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=..."}'userId comes from the credential. Unknown fields are stripped. Reusing merchantStoreId returns409. Duplicate store names are also disallowed, but currently surface as 500 rather than a clean conflict.
/stores/update201Partially update or deactivate a store.
scope store:updateName the store at the top level and put changed values underchanges. This is intentionally not a PATCH or PUT route, just as reads go through POST /stores/detail.
- changes field
name- Type
- string
- Notes
- Same rules as create
- changes field
paywayLink- Type
- URL string
- Notes
- Same https, host, and USD rules as create. Used for future payments
- changes field
merchantStoreId- Type
- string
- Notes
- Must remain unique within the account
- changes field
webhookUrl- Type
- URL string
- Notes
- Stored for future delivery
- changes field
webhookSecret- Type
- string
- Notes
- Stored and never returned
- changes field
active- Type
- boolean
- Notes
- False blocks new payments
| changes field | Type | Notes |
|---|---|---|
name | string | Same rules as create |
paywayLink | URL string | Same https, host, and USD rules as create. Used for future payments |
merchantStoreId | string | Must remain unique within the account |
webhookUrl | URL string | Stored for future delivery |
webhookSecret | string | Stored and never returned |
active | boolean | False blocks new payments |
curl -X POST https://api.bongluy.com/stores/update \
-H 'authorization: Bearer sk_live_...' \
-H 'content-type: application/json' \
-d '{"merchantStoreId":"branch-2","changes":{"active":false}}'To rename a merchant id, use the old value at the top level and the new value inside changes. An empty object, or one containing only misspelled fields, returns400 "No fields to update".
Missing and foreign stores both return404 "Store not found". Renaming into another store's merchant id returns 409.
Create one payment per checkout.
The create call reserves a KHQR and snapshots the store's PayWay link onto a durable payment record.
/payment201Create a payment and reserve its KHQR.
scope payment:create- Field
amount- Type
- string
- Required
- Yes
- Notes
- Decimal with zero, one, or two fractional digits
- Field
storeId / merchantStoreId- Type
- string
- Required
- Either
- Notes
- The active store receiving funds
- Field
tranId- Type
- string
- Required
- No
- Notes
- Your transaction id and idempotency key
- Field
currency- Type
- string
- Required
- No
- Notes
- Metadata only; defaults to USD
| Field | Type | Required | Notes |
|---|---|---|---|
amount | string | Yes | Decimal with zero, one, or two fractional digits |
storeId / merchantStoreId | string | Either | The active store receiving funds |
tranId | string | No | Your transaction id and idempotency key |
currency | string | No | Metadata only; defaults to USD |
curl -X POST https://api.bongluy.com/payment \
-H 'authorization: Bearer sk_live_...' \
-H 'content-type: application/json' \
-d '{"merchantStoreId":"branch-2","amount":"12.50","tranId":"INV-1042"}'{
"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://ababank.com?type=payway&qrcode=...",
"android": "intent://ababank.com?type=payway&qrcode=...#Intent;package=com.paygo24.ibank;..."
}
}- Field
id- Type
- string
- Meaning
- Bongluy's UUID for the payment. Pass it back as id on the status and detail routes.
- Field
storeId- Type
- string
- Meaning
- The store that receives the funds.
- Field
status- Type
- string
- Meaning
- PENDING, SUCCESS, EXPIRED, or FAILED. Always PENDING at creation.
- Field
amount- Type
- string
- Meaning
- The decimal you asked for, echoed back unchanged.
- Field
currency- Type
- string
- Meaning
- Label only. It does not instruct PayWay or convert funds.
- Field
paywayLink- Type
- string
- Meaning
- The store's PayWay link as it was at creation. Later edits to the store do not rewrite it.
- Field
checkoutUrl- Type
- string
- Meaning
- Hosted Bongluy checkout page for this payment. Safe to send straight to a payer.
- Field
expireAt- Type
- string
- Meaning
- ISO 8601. After this instant the payment can no longer be paid upstream.
- Field
qrString- Type
- string | null
- Meaning
- The KHQR payload. Encode it yourself to draw the QR.
- Field
deeplink- Type
- object | null
- Meaning
- scheme opens ABA Mobile on iOS; android is an intent URL. Null whenever qrString is null.
- Field
tranId- Type
- string | null
- Meaning
- Your transaction id, exactly as you sent it.
- Field
settledTranId- Type
- string | null
- Meaning
- ABA's id for the completed transaction. Null until SUCCESS.
- Field
receiptUrl- Type
- string | null
- Meaning
- ABA receipt link. Null until SUCCESS.
- Field
settledAt- Type
- string | null
- Meaning
- ISO 8601 instant of settlement. Null until SUCCESS.
- Field
lastError- Type
- string | null
- Meaning
- The most recent upstream failure for this payment, if any.
- Field
createdAt- Type
- string
- Meaning
- ISO 8601 timestamp.
- Field
updatedAt- Type
- string
- Meaning
- ISO 8601 timestamp of the last change to the record.
| Field | Type | Meaning |
|---|---|---|
id | string | Bongluy's UUID for the payment. Pass it back as id on the status and detail routes. |
storeId | string | The store that receives the funds. |
status | string | PENDING, SUCCESS, EXPIRED, or FAILED. Always PENDING at creation. |
amount | string | The decimal you asked for, echoed back unchanged. |
currency | string | Label only. It does not instruct PayWay or convert funds. |
paywayLink | string | The store's PayWay link as it was at creation. Later edits to the store do not rewrite it. |
checkoutUrl | string | Hosted Bongluy checkout page for this payment. Safe to send straight to a payer. |
expireAt | string | ISO 8601. After this instant the payment can no longer be paid upstream. |
qrString | string | null | The KHQR payload. Encode it yourself to draw the QR. |
deeplink | object | null | scheme opens ABA Mobile on iOS; android is an intent URL. Null whenever qrString is null. |
tranId | string | null | Your transaction id, exactly as you sent it. |
settledTranId | string | null | ABA's id for the completed transaction. Null until SUCCESS. |
receiptUrl | string | null | ABA receipt link. Null until SUCCESS. |
settledAt | string | null | ISO 8601 instant of settlement. Null until SUCCESS. |
lastError | string | null | The most recent upstream failure for this payment, if any. |
createdAt | string | ISO 8601 timestamp. |
updatedAt | string | ISO 8601 timestamp of the last change to the record. |
checkoutUrl is the hosted Bongluy checkout page for this payment. It is safe to hand to a payer directly, so an integration that does not want to render its own QR can redirect to it or share it as a link.
Money on the wire
amount is a string because decimal money should not pass through JSON floating-point values. It must match^\d+(\.\d{1,2})?$. The API currently accepts "0" and "0.00", so enforce your own minimum before calling it.
currency labels the record; it does not instruct PayWay or convert funds. The actual charge follows the store's PayWay link. A mismatched value only mislabels your own data.
Use tranId for safe retries
tranId is unique within a store. Repeating a create request with an existing tranId returns the original payment verbatim, without a second upstream call. This makes a timed-out create safe to retry. Both payment read routes also accept tranId, so your system can work entirely with its own store and transaction identifiers.
Payments without tranId do not collide, but a lost create response cannot then be recovered through your own identifier, which leaves Bongluy's UUID as the only way back to the payment. Send a tranId on every create and that case disappears.
settledTranId is different. It is ABA's identifier for the completed transaction and appears only after success.You do not need to store Bongluy's ids
Integrating requires no new table, no migration, and no new columns for id or storeId. Every route can be addressed with identifiers your system already owns, so Bongluy's UUIDs never have to be persisted.
- Your existing identifier
- Your store, branch, or outlet id
- Field you send
merchantStoreId- Accepted by
- Every store-taking route, in place of storeId
- Your existing identifier
- Your order, invoice, or sale id
- Field you send
tranId- Accepted by
- POST /payment, /payment/status, /payment/detail
| Your existing identifier | Field you send | Accepted by |
|---|---|---|
| Your store, branch, or outlet id | merchantStoreId | Every store-taking route, in place of storeId |
| Your order, invoice, or sale id | tranId | POST /payment, /payment/status, /payment/detail |
Register merchantStoreId when the store is created and send a tranId on every payment. That pair then addresses the payment for the rest of its life — status, detail, and retries alike. Bongluy stays the system of record for payment state; read it back with the pair rather than mirroring it locally.
merchantStoreId can only be addressed by itsstoreId, and the exact lookup bytranId is POST /payment/detail.Expiry is part of the response
ABA supplies the lifetime; Bongluy falls back to 180 seconds only when ABA omits it. Read expireAt from every payment response instead of hardcoding three minutes. A payment past that timestamp cannot be paid upstream.
An inactive, missing, or foreign store returns404 "unknown store". The response deliberately does not reveal which condition applied.
/paymentList one store's payments, newest first.
scope payment:read- Query
storeId / merchantStoreId- Default
- -
- Notes
- Either store identifier is required
- Query
page- Default
- 1
- Notes
- Integer at least 1
- Query
per_page- Default
- 25
- Notes
- Integer from 1 to 100; snake_case
- Query
status- Default
- -
- Notes
- PENDING, SUCCESS, EXPIRED, or FAILED
| Query | Default | Notes |
|---|---|---|
storeId / merchantStoreId | - | Either store identifier is required |
page | 1 | Integer at least 1 |
per_page | 25 | Integer from 1 to 100; snake_case |
status | - | PENDING, SUCCESS, EXPIRED, or FAILED |
curl 'https://api.bongluy.com/payment?merchantStoreId=branch-2&status=SUCCESS&per_page=50' \
-H 'authorization: Bearer sk_live_...'{
"data": [
{
"id": "8435481a-48a8-4bb2-91d2-bcd1e604fb17",
"status": "SUCCESS",
"amount": "12.50",
"tranId": "INV-1042"
}
],
"page": 1,
"per_page": 50,
"total": 137,
"total_pages": 3
}- Field
data- Type
- array
- Meaning
- Payment objects, newest first. Each one is abbreviated above but complete in the real response.
- Field
page- Type
- number
- Meaning
- The page you are on, echoing the page query.
- Field
per_page- Type
- number
- Meaning
- How many records this page can hold. Note the snake_case.
- Field
total- Type
- number
- Meaning
- Total matching payments across every page.
- Field
total_pages- Type
- number
- Meaning
- How many pages exist at this per_page. Stop when page reaches it.
| Field | Type | Meaning |
|---|---|---|
data | array | Payment objects, newest first. Each one is abbreviated above but complete in the real response. |
page | number | The page you are on, echoing the page query. |
per_page | number | How many records this page can hold. Note the snake_case. |
total | number | Total matching payments across every page. |
total_pages | number | How many pages exist at this per_page. Stop when page reaches it. |
Each element of data is the same payment object the create route returns, checkoutUrl included, so a payment link can be recovered from the list without a second lookup.
Spelling per_page as perPage does not fail; the unknown query is stripped and the response silently uses 25. There is no list filter for tranId. Use the exact detail lookup instead.
A deactivated store's history remains listable. Ownership, not active state, controls access to existing payments.
The checkout owns the QR.
The create response gives you the raw material for desktop, point-of-sale, and mobile checkout. Keep the KHQR payload as the source of truth.
Render qrString
Encode qrString with any QR library and show the result with the payment amount and expiry. On desktop it is the primary path; on mobile it remains the fallback if the banking app cannot open.
Offer the ABA Mobile deeplink when present
deeplink.scheme opens ABA Mobile on iOS.deeplink.android is an intent URL that can fall through to the Play Store. The iOS scheme silently does nothing when the app is absent.
const isAndroid = /android/i.test(navigator.userAgent);
const href = isAndroid
? payment.deeplink.android
: payment.deeplink.scheme;qrString as authoritative.deeplink is null whenever qrString is null.
Poll for speed, read detail for truth.
The status route is the lightweight observation channel. The detail route is the durable record used for receipts and reconciliation.
/payment/status201Read the current payment state.
scope payment:readSend a store identifier plus either the payment UUID inid or your own tranId. If both payment identifiers are present, id wins.
curl -X POST https://api.bongluy.com/payment/status \
-H 'authorization: Bearer sk_live_...' \
-H 'content-type: application/json' \
-d '{"merchantStoreId":"branch-2","tranId":"INV-1042"}'{
"paymentId": "8435481a-48a8-4bb2-91d2-bcd1e604fb17",
"status": "SUCCESS",
"expireAt": 1786763662418,
"settledTranId": "1234567890",
"receipt": "https://...",
"at": 1786763501992
}- Field
paymentId- Type
- string
- Meaning
- Bongluy's UUID for the payment. Named id on every other route.
- Field
status- Type
- string
- Meaning
- PENDING, SUCCESS, EXPIRED, or FAILED.
- Field
expireAt- Type
- number
- Meaning
- Epoch milliseconds, not the ISO string the payment object uses.
- Field
at- Type
- number
- Meaning
- Epoch milliseconds when this reading was taken. Useful for ordering poll results.
- Field
settledTranId- Type
- string
- Meaning
- ABA's transaction id. The key is absent, not null, while pending.
- Field
receipt- Type
- string
- Meaning
- ABA receipt link, named receiptUrl on the payment object. Absent while pending.
| Field | Type | Meaning |
|---|---|---|
paymentId | string | Bongluy's UUID for the payment. Named id on every other route. |
status | string | PENDING, SUCCESS, EXPIRED, or FAILED. |
expireAt | number | Epoch milliseconds, not the ISO string the payment object uses. |
at | number | Epoch milliseconds when this reading was taken. Useful for ordering poll results. |
settledTranId | string | ABA's transaction id. The key is absent, not null, while pending. |
receipt | string | ABA receipt link, named receiptUrl on the payment object. Absent while pending. |
expireAt and at are epoch milliseconds, and the receipt field is namedreceipt. While pending,settledTranId and receipt are absent, not null.Recent payments are served from a fast in-memory snapshot for the first 15 minutes and fall back to the stored row later. The route remains usable indefinitely; only latency changes.
Created and awaiting payment.
Paid and settled.
Expired without payment.
Reserved for a future non-success terminal state.
Stop at the deadline
Poll every two or three seconds until status leavesPENDING or the returned expiry time passes. There is no streaming or long-polling endpoint.
async function waitForPayment({ merchantStoreId, tranId, expireAt }) {
const deadline = new Date(expireAt).getTime();
while (Date.now() < deadline) {
const response = await fetch("https://api.bongluy.com/payment/status", {
method: "POST",
headers: {
authorization: `Bearer ${process.env.BONGLUY_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({ merchantStoreId, tranId }),
});
const { status } = await response.json();
if (status !== "PENDING") return status;
await new Promise((resolve) => setTimeout(resolve, 2500));
}
return "EXPIRED";
}PENDING after expireAt. Treat it as unpaid, stop checkout polling, and reconcile later. Do not leave the payer watching an endless spinner.At 2.5-second intervals, one three-minute checkout consumes about 72 calls. Roughly eight concurrent checkouts can saturate a 600-request-per-minute key. Poll once per payment, not from every open browser tab.
/payment/detail201Read the complete durable payment record.
scope payment:readThe lookup body is identical to the status route, so your ownmerchantStoreId and tranId are enough to retrieve it. The response matches the create response, including amount, currency, tranId,receiptUrl, settledAt, ISO timestamps, QR, and deeplinks.
curl -X POST https://api.bongluy.com/payment/detail \
-H 'authorization: Bearer sk_live_...' \
-H 'content-type: application/json' \
-d '{"merchantStoreId":"branch-2","tranId":"INV-1042"}'A store that is absent or belongs to another account returns404 "unknown store". A payment that is absent from a store you own returns404 "unknown payment". Foreign resources are deliberately indistinguishable from missing ones.
Errors are JSON, with one important variation.
Most failures use a statusCode, message, and error envelope. Validation changes message from a string to an array of strings.
{
"statusCode": 400,
"message": [
"amount must be a positive decimal",
"storeId must be a UUID"
],
"error": "Bad Request"
}- Field
statusCode- Type
- number
- Meaning
- Repeats the HTTP status, so you can read it without inspecting the response object.
- Field
message- Type
- string | string[]
- Meaning
- One sentence for most failures; an array of field complaints for validation. Branch on the type before displaying it.
- Field
error- Type
- string
- Meaning
- The HTTP reason phrase, such as Bad Request. Not a stable machine-readable code.
| Field | Type | Meaning |
|---|---|---|
statusCode | number | Repeats the HTTP status, so you can read it without inspecting the response object. |
message | string | string[] | One sentence for most failures; an array of field complaints for validation. Branch on the type before displaying it. |
error | string | The HTTP reason phrase, such as Bad Request. Not a stable machine-readable code. |
Either/or identifier pairs can produce several messages for one omission. If neither store identifier is present, validation may complain about both storeId andmerchantStoreId; supplying either resolves the group.
- Status
400- Meaning
- Validation failure, or a store update with no recognized changes
- Status
401- Meaning
- Missing, invalid, expired, revoked, or under-scoped API key
- Status
403- Meaning
- Unrecognized origin on a public route
- Status
404- Meaning
- Missing or foreign store/payment
- Status
409- Meaning
- merchantStoreId already in use
- Status
429- Meaning
- Rate limit exceeded
- Status
500- Meaning
- PayWay failure, server failure, or currently a duplicate store name
| Status | Meaning |
|---|---|
400 | Validation failure, or a store update with no recognized changes |
401 | Missing, invalid, expired, revoked, or under-scoped API key |
403 | Unrecognized origin on a public route |
404 | Missing or foreign store/payment |
409 | merchantStoreId already in use |
429 | Rate limit exceeded |
500 | PayWay failure, server failure, or currently a duplicate store name |
401 Unauthorized. When a previously working key starts failing, check its 90-day age before changing request code.PayWay failures from POST /payment currently appear as 500, not 502 or 503. Retry with the same tranId so a request that actually succeeded upstream cannot create a duplicate payment.
Rate limits
- Authenticated API
- 600 requests per minute, per API key. The 429 response has no Retry-After header; back off for the rest of the minute.
- Store writes
- 20 per minute, per account. Creating and updating stores is setup work, not per-sale work.
- Checkout page, per payment
- 20 requests per second. This governs the hosted checkout a payer has open, not your server's polling.
- Checkout page, overall
- 200 requests per second across all payments.
Better Auth key-management endpoints use their own envelope,{ "code": "...", "message": "..." }. Store and payment routes use the standard status-code shape above.
Webhooks are not available yet.
Poll POST /payment/status for checkout feedback and read POST /payment/detail when you need the durable record.
webhookUrl and webhookSecret are accepted on stores and stored, but nothing reads them. Configuring them today will not produce a delivery.Per-store webhooks and a signed payload are planned, but the payload and signature scheme are deliberately undocumented until they ship. Polling remains the supported integration contract.
A server-configured global delivery endpoint may exist in some deployments, but it is not exposed to merchants and is not part of this API contract.