Skip to main content

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:

  1. Verify the signature (below) before trusting the payload.
  2. Return 2xx quickly. Do the real work asynchronously — acknowledge first, process after.
  3. 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:

EventMeaning
PAYMENT_CAPTUREDThe customer paid. Funds are captured.
PAYMENT_AUTHORIZEDApproved and held, awaiting completion (two-step terminals only).
PAYMENT_REFUSEDDeclined at the terminal.
PAYMENT_CANCELLEDThe bill was cancelled before payment.
PAYMENT_ERRORThe 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-status call 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.