Card payments are asynchronous — 3-D Secure completes in a browser you don't control, refunds settle later, subscription charges happen on a schedule. Webhooks are how you find out, without polling.
Register your endpoint
POST /v1/webhook_endpoints with your receiving URL and the event types you want. HTTPS only, and it must accept POST and return a 2xx within 10 seconds.
Verify the signature
Every delivery includes a Paayed-Signature header with an HMAC-SHA256 of the body, signed with your endpoint's own secret. Always verify before processing.
Process asynchronously
Acknowledge with a 2xx quickly and do heavy work in the background. Our delivery worker retries on timeout, and duplicates are expected under at-least-once delivery — treat the event id as an idempotency key on your side.
Names are noun then past-tense verb, dot separated, and stable forever.
payment.created, payment.requires_action, payment.authorized, payment.captured, payment.succeeded, payment.declined, payment.failed, payment.cancelled, payment.expired, payment.settled
refund.created, refund.succeeded, refund.failed
hosted_session.completed, hosted_session.expired
customer.created, customer.updated, customer.deleted, payment_method.attached, payment_method.detached, payment_method.expiring
subscription.created, subscription.charged, subscription.charge_failed, subscription.paused, subscription.resumed, subscription.cancelled
dispute.opened, dispute.updated, dispute.closed, payout.paid, payout.failed — names reserved now, delivered in phase two.
Every event ships the full object, not a thin notification — a snapshot at the moment the event fired, including what changed:
{
"id": "evnt_01JBQ7YM9C1XB6TZQ4",
"object": "event",
"type": "payment.succeeded",
"api_version": "2026-09-01",
"created_at": "2026-09-04T10:31:07Z",
"livemode": true,
"account": "merch_9Hs2Kd41Qm",
"sequence": 3,
"data": {
"object": { "id": "pay_01JBQ7X2F9KDNW3M8T", "object": "payment", "status": "succeeded", "amount": 42.50 },
"previous_attributes": { "status": "requires_action" }
}
}signed_payload = timestamp + "." + raw_request_body
signature = hex( hmac_sha256( endpoint_secret, signed_payload ) )Paayed-Signature: t=1757001600,v1=8d5c3ab1...c41fRead the raw body before parsing JSON — a re-serialized body won't match, and this is the single most common integration mistake. Reject a timestamp more than five minutes old to stop replay of a captured request.
During a secret rotation both the old and new secret are sent, so you can deploy the new secret with no downtime:
Paayed-Signature: t=1757001600,v1=<new secret>,v1=<old secret>| Attempt | Delay after the previous one |
|---|---|
| 1 | Immediately |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
| 7 | 12 hours |
| 8 | 24 hours |
Eight attempts over about forty-eight hours, each wait lengthened by up to a tenth at random so a recovering endpoint isn't hit with every backlogged delivery at once. Every attempt is recorded at GET /v1/webhook_endpoints/{id}/deliveries. After a day of continuous failure the endpoint is marked degraded: it keeps receiving, but each new delivery waits five minutes before its first attempt, and one success clears the state. After a week it's disabled and stops receiving, and your account administrators are emailed. Fix the endpoint, PATCH it with "status": "enabled", then replay what was missed with POST /v1/webhook_endpoints/{id}/replay and { "since": "2026-09-15T00:00:00Z" }, optionally narrowed with "types", up to thirty days back.
If you ever suspect a missed delivery, GET /v1/events is the backstop — retained for 30 days regardless of webhook status.
Check the endpoint is publicly reachable over HTTPS. Send a test event per event type from the dashboard's delivery log, and check the recorded response there — status code, timing, and the first kilobyte of your response are all shown.
Use the raw request body for verification, not a parsed-and-re-serialized version. Confirm you're using the endpoint's own signing secret, not your API key.
Ordering isn't guaranteed. Every event carries a sequence that increases for each event on the same object — ignore an event whose sequence isn't greater than the last one you applied. Better still, treat a delivery as a hint and read the object with GET.