Webhooks
Receiving and verifying event notifications.
Webhooks push events to your systems as they happen — payment results, refunds, disputes, subscription invoices — so you never poll.
Setup
Add an HTTPS endpoint in the Control Center and select the events you want. Each endpoint has a webhook secret used to sign deliveries.
Event format
The body carries the event type plus the full resource object it concerns:
{
"merchant_id": "merchant_1234567890",
"event_id": "evt_018f2c1b9a7e",
"event_type": "payment_succeeded",
"content": {
"type": "payment_details",
"object": {
"payment_id": "pay_V4t1RJgSy4hoBQzoqqRz",
"status": "succeeded",
"amount": 4900,
"currency": "EUR"
}
},
"timestamp": "2026-07-13T10:24:31Z"
}
content.type tags what kind of object the payload holds (payment_details, refund_details, dispute_details, …); event_type says what happened to it.
Key event types:
| Event type | Fired when |
|---|---|
payment_succeeded / payment_failed |
A payment reaches a final state |
payment_processing |
Payment sent to the processor, result pending (e.g. crypto confirmations) |
action_required |
Customer action needed (e.g. 3DS challenge at checkout) |
payment_cancelled / payment_expired |
Payment voided or timed out |
refund_succeeded / refund_failed |
Refund completes |
dispute_opened / dispute_challenged / dispute_won / dispute_lost |
Chargeback lifecycle |
mandate_active / mandate_revoked |
Saved-payment-method mandates |
invoice_paid |
A subscription billing period was charged successfully |
Verifying signatures
Every delivery carries an X-Webhook-Signature-512 header: the hex-encoded HMAC-SHA512 of the raw request body, keyed with your webhook secret.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: Buffer, signatureHeader: string, secret: string): boolean {
const expected = createHmac("sha512", secret).update(rawBody).digest("hex");
const received = Buffer.from(signatureHeader, "hex");
const wanted = Buffer.from(expected, "hex");
return received.length === wanted.length && timingSafeEqual(received, wanted);
}
Always verify against the raw request body — parsing and re-serializing JSON will break the signature. In Express, use express.raw({ type: "application/json" }) on the webhook route, not express.json().
Delivery & retries
- Respond with a
2xxpromptly; do heavy work asynchronously. - Failed deliveries are retried with backoff.
- Deliveries can arrive out of order and more than once — treat handlers as idempotent, using
event_idas your deduplication key. - The Control Center shows each webhook's delivery attempts with response codes, and lets you redeliver manually.