본문으로 건너뛰기

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.

Webhooks and polling together​

Webhooks are your durable record of every outcome; the status call is what keeps a live cashier screen honest. The two overlap safely:

  • 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.