Webhooks
A webhook is how Boundless tells your application that a payment reached a final state, without you having to poll. For in-store payments this is the moment the customer finished at the terminal.
Setting up
Register an HTTPS endpoint in the Boundless dashboard, or over the API
(POST /api/webhooks/configs — see the API reference).
Either way you receive a signing secret; treat it like a password. You can send yourself a
TEST event at any time to prove the wiring end to end.
Boundless will POST each event to your endpoint. Your endpoint should:
- Verify the signature (below) before trusting the payload.
- Return
2xxquickly. Do the real work asynchronously — acknowledge first, process after. - Be idempotent. An event may be delivered more than once; deduplicate on the delivery's
eventId, and apply each payment outcome once.
What a delivery looks like
POST <your endpoint>
Content-Type: application/json
X-Webhook-Signature: <Base64 HMAC-SHA256 of the raw body>
X-Webhook-Event-Type: PAYMENT_CAPTURED
X-Webhook-Event-Id: <delivery id>
{
"eventId": "evt_1a2b3c4d5e6f7a8b",
"eventType": "PAYMENT_CAPTURED",
"merchantExternalId": "your-merchant-id",
"timestamp": "2026-08-14T06:20:11Z",
"data": {
"pspReference": "BNDL7K2P9QW1ABCD",
"merchantReference": "order-4821",
"amount": "100.00",
"currency": "SGD",
"status": "CAPTURED",
"cardSummary": "****4242",
"cardBrand": "VISA",
"paymentMethod": "CARD"
}
}
merchantReference is the reference you set when creating the payment — your key for matching
the event back to the bill. Payloads never contain full card numbers; card data appears only as a
masked summary.
Events you will receive
For the in-store lifecycle:
| Event | Meaning |
|---|---|
PAYMENT_CAPTURED | The customer paid. Funds are captured. |
PAYMENT_AUTHORIZED | Approved and held, awaiting completion (two-step terminals only). |
PAYMENT_REFUSED | Declined at the terminal. |
PAYMENT_CANCELLED | The bill was cancelled before payment. |
PAYMENT_ERROR | The terminal reported a problem. |
The same webhook channel also carries later lifecycle events — refunds and settlement — using the
PAYMENT_REFUNDED, PAYMENT_SETTLED, and related event types. Subscribe to the events your
integration acts on.
Verifying the signature
Each delivery is signed with your secret using HMAC-SHA256, Base64-encoded, over the raw request
body. Compute the same value and compare it, in constant time, against X-Webhook-Signature.
Reject any request whose signature does not match — an unsigned or wrongly signed request did not
come from Boundless.
import base64, hashlib, hmac
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = base64.b64encode(
hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, signature_header)
Verify against the raw bytes of the body, before any JSON parsing or re-serialisation, so your computed signature matches exactly. When you rotate the signing secret, deliveries are signed with the new secret from that moment on — roll the new value out to every instance of your handler before rotating, or accept both values briefly while your own deployment completes.
Webhook first, polling as backup
Treat webhooks as your primary outcome path and the status call as the fallback:
- If a webhook is delayed or your endpoint was briefly down, a status call gives you the same answer.
- A
sync-statuscall arriving at the same moment as a webhook is safe — the payment settles once, whichever reaches it first.
This is why a captured payment stays captured no matter how many times, or how many ways, you ask about it.