Accept a payment
Taking an in-store payment is two steps: start it, then follow it to its outcome.
Step 1 — Start the payment
Send the amount to a store and terminal you have registered:
curl https://api-live.kcpboundless.com/api/in-store/payments \
-H "X-API-Key: bndl_live_..." \
-H "Content-Type: application/json" \
-d '{
"merchantExternalId": "your-merchant-id",
"storeExternalId": "your-store-id",
"terminalExternalId": "your-terminal-id",
"amount": 100.00,
"currency": "SGD",
"reference": "order-4821"
}'
| Field | Required | Description |
|---|---|---|
merchantExternalId | yes | Your business identifier. |
storeExternalId | yes | The store the payment is taken at. |
terminalExternalId | yes | The device at that store. |
amount | yes | Decimal amount in the payment's currency, greater than zero. |
currency | yes | ISO 4217 code, for example SGD. |
reference | recommended | Your own reference for the bill. Use it to match the outcome back to your records. |
sequenceNumber | optional | Position of this tender when one bill is split across several payments. |
requestedModes | optional | Restrict what the terminal offers: ["CARD"] for cards, ["QR_CODE"] for PayNow and other QR tenders. Omit to allow every mode the device supports. See Choosing the tender. |
terminalTimeoutMinutes | optional | How long the device waits for the customer. |
cashierId, customerMobile, customerEmail | optional | Passed through for the receipt and your records. |
The response is the payment, waiting at the terminal:
{
"id": "01997b3e-6c31-7a42-9f10-4d2c8b5e7a01",
"pspReference": "BNDL7K2P9QW1ABCD",
"status": "AWAITING_TERMINAL",
"amount": 100.00,
"currency": "SGD"
}
id— the payment id. Use it to check status. It is an opaque string (a UUID); store it as text and pass it back unchanged. Do not parse it as a number.pspReference— Boundless's stable reference for the payment.status—AWAITING_TERMINALmeans the bill is on the device and no money has moved yet.
Store the payment id against the bill in your own database before the cashier sees the
waiting screen. If your till crashes between the call and the write, you have lost the handle on a
live payment.
The customer now taps their card at the terminal.
Step 2 — Follow it to the outcome
You learn the result two ways, and a good integration uses both: poll sync-status for the live
cashier screen, and subscribe to webhooks as your durable record of every outcome — see
A recommended till flow below.
By webhook
Register a webhook endpoint (see Webhooks) and Boundless will call it when the payment reaches a final state — captured, declined, cancelled. You do not have to poll.
By status call
Two calls answer "where is this payment now", and they differ in cost:
Read the current state — a plain lookup of what Boundless already knows. Cheap; use this for routine polling:
curl https://api-live.kcpboundless.com/api/transactions/01997b3e-6c31-7a42-9f10-4d2c8b5e7a01 \
-H "X-API-Key: bndl_live_..."
Refresh from the terminal — asks the acquirer what happened at the device and applies the
answer before responding. This is the call for the live cashier screen: while a cashier is watching
the terminal, calling it on a short interval — every two to three seconds is reasonable — until the
payment leaves AWAITING_TERMINAL gives the fastest honest answer. It is also the call for when
you suspect Boundless's view is behind the device — for example the customer says they paid but the
payment still reads as waiting:
curl -X POST https://api-live.kcpboundless.com/api/in-store/payments/01997b3e-6c31-7a42-9f10-4d2c8b5e7a01/sync-status \
-H "X-API-Key: bndl_live_..."
Either way the response is the payment, with its status advanced if the customer has finished:
{
"id": "01997b3e-6c31-7a42-9f10-4d2c8b5e7a01",
"status": "CAPTURED",
"amount": 100.00,
"currency": "SGD",
"cardBrand": "VISA",
"last4": "4242"
}
Both calls are safe to make repeatedly. If the payment is still open, you get AWAITING_TERMINAL
again; once it is final, the state does not change on you. A sensible point of sale combines a
webhook subscription with one sync-status behind its "check payment" button.
Every payment ends
Boundless also runs a reconciliation sweep over in-store payments on its own, independently of anything your point of sale does. It picks up outcomes your till never asked about — including payments cancelled or completed at the device — and closes out bills that nobody decided.
The sweep runs on a cycle of a few minutes, and only considers a payment once its own terminal timeout has elapsed plus a short grace period. It is a safety net that guarantees every payment eventually reaches a final state, even if your till goes offline mid-sale.
It is not a mechanism to build a cashier experience on. The delay is measured in minutes, not
seconds. When you need an answer now, call sync-status.
A recommended till flow
- Create the payment; show the cashier a waiting screen.
- Poll
sync-statusevery few seconds while the screen is up. - Resolve the screen as soon as the payment leaves
AWAITING_TERMINAL. - Give the cashier an explicit "check payment" button that calls
sync-status— the answer to "the customer says it went through". - Never leave a waiting screen with no exit. The background sweep guarantees the payment resolves eventually; your UI must be able to reach that resolution.
- Stop polling once the payment is in a final state. Continued polling of a decided payment does nothing and consumes your rate-limit budget.
Register a webhook alongside the polling: the webhook is your durable record of the outcome — delivered even if the till was offline when the payment resolved — while the polling is what keeps the cashier's screen honest.
Outcomes
| Status | Meaning | What your till should do |
|---|---|---|
CREATED | Accepted by the platform, not yet at the device. | Keep waiting. |
AWAITING_TERMINAL | On the device, waiting for the customer. Not yet paid. | Keep waiting. Show "follow the terminal" to the cashier. |
CAPTURED | Paid. Funds are captured. | Close the bill, print the slip. |
AUTHORIZED | Approved and held, awaiting completion. Applies only to terminals configured for a two-step authorize-then-complete flow. | Treat as paid for till purposes. |
REFUSED | Declined at the terminal. | Offer another tender. |
CANCELLED | Withdrawn before completion — by your point of sale, at the device itself, or reversed by a void. | Close the bill as unpaid. |
EXPIRED | Nobody completed the bill in time; Boundless closed it automatically. | Close the bill as unpaid. |
ERROR, FAILED | A processing problem prevented a decision. | Safe to retry the create with the same reference. |
Branch on status, not on the HTTP status alone — see
one bill on the device at a time for the important case
where a 201 Created carries a REFUSED payment.
Further statuses exist for post-sale processing. They are visible on a payment after the sale, but no till action depends on them.
Retries, duplicates, and split bills
Point-of-sale software retries: networks drop, cashiers double-press, apps restart mid-request. The API is built for that.
- One open payment per reference. While a payment carrying your
referenceis still open — waiting at the terminal or authorized — sending the same create request again returns that same payment rather than creating a duplicate. Retry a timed-out create with the samereferencewith confidence. Once the payment reaches a final state, the same reference may be used again for a genuinely new bill. - A failed payment is retry-safe.
ERRORandFAILEDare final, so re-creating with the samereferencestarts a fresh payment — the guarantee above ensures you never end up with two live ones. - Split bills. When one bill is settled by several tenders, keep the same
referenceand give each tender its ownsequenceNumber(1, 2, 3…). Each reference-and-sequence pair gets its own one-open-payment guarantee. - One bill on the device at a time. A terminal shows one bill. Starting a second payment
(different reference) while the first is still on the device does not return an HTTP error — the
create responds normally with the new payment already
REFUSEDand the terminal provider's explanation attached. Complete or cancel the open bill, then create again. Always branch on the returnedstatus, never on the HTTP code alone.
Retry policy
| Condition | Retry? |
|---|---|
| Network timeout, no response | Yes — same reference |
5xx | Yes, with backoff — same reference |
429 | Yes, after Retry-After |
4xx other than 429 | No. Fix the request. |
200 / 201 with status ERROR or FAILED | Yes — same reference |
200 / 201 with status REFUSED | No. The payment was decided. Offer another tender. |
Good practice
- Match on your
reference. Every outcome carries the payment back to the bill you started, so set areferenceyou can look up. The reference is also printed on the customer's payment slip, which lets store staff read a bill's reference straight off the paper when reconciling. - Do not assume from the start call. A
201on the start request means the bill reached the device, not that the customer paid. The payment is only paid atCAPTURED. - A bill nobody completes is closed automatically. If a customer walks away, Boundless resolves the payment on its own after the terminal's timeout — you do not have to reconcile abandoned bills yourself.
Choosing the tender
requestedModes decides what the terminal offers the customer:
| Value | The device offers |
|---|---|
["CARD"] | Card only |
["QR_CODE"] | PayNow and other QR tenders only |
| omitted, or several values | Everything the device supports; the customer picks |
Naming a single mode does more than narrow the screen — it tells Boundless what the payment is before the customer pays, so the payment is recorded and priced as that tender from the start:
curl -X POST https://api-live.kcpboundless.com/api/in-store/payments \
-H "X-API-Key: bndl_live_..." \
-H "Content-Type: application/json" \
-d '{
"merchantExternalId": "your-merchant-id",
"storeExternalId": "your-store-id",
"terminalExternalId": "your-terminal-id",
"amount": 18.50,
"currency": "SGD",
"reference": "order-4821",
"requestedModes": ["QR_CODE"]
}'
If you leave the choice to the customer, the payment starts as a card payment and Boundless
corrects it to the tender the terminal reports once they have paid. Prefer naming the mode when
your checkout already knows it — a PayNow button in your till should send ["QR_CODE"].
Which tenders a device can present is part of its setup with Boundless. Asking for a mode the device does not offer fails the request rather than silently falling back to another tender.
Cancelling an open bill
If the customer changes their mind before paying, withdraw the bill from the device:
curl -X POST https://api-live.kcpboundless.com/api/in-store/payments/01997b3e-6c31-7a42-9f10-4d2c8b5e7a01/cancel \
-H "X-API-Key: bndl_live_..."
The response is the payment in its resulting state — normally CANCELLED.
The cancel request and the customer's tap can race each other. If the customer completed the payment
first, their payment stands and the response comes back CAPTURED, not CANCELLED. Always read
the returned status rather than assuming the cancellation took effect.
That is the short version. The full picture — cancellations made at the device itself, undoing a sale the customer already paid (a void), and the rules that govern both — is in Reverse a payment.