Home
Integration

Webhooks

Signed event delivery for payments, refunds, subscriptions and more.

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.

Setup

1

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.

2

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.

3

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.

Event catalogue

Names are noun then past-tense verb, dot separated, and stable forever.

Payment events

payment.created, payment.requires_action, payment.authorized, payment.captured, payment.succeeded, payment.declined, payment.failed, payment.cancelled, payment.expired, payment.settled

Refund events

refund.created, refund.succeeded, refund.failed

Hosted session events

hosted_session.completed, hosted_session.expired

Customer & payment method events

customer.created, customer.updated, customer.deleted, payment_method.attached, payment_method.detached, payment_method.expiring

Subscription events

subscription.created, subscription.charged, subscription.charge_failed, subscription.paused, subscription.resumed, subscription.cancelled

Dispute & payout events

dispute.opened, dispute.updated, dispute.closed, payout.paid, payout.failed — names reserved now, delivered in phase two.

Payload

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" }
  }
}

Signing

signed_payload = timestamp + "." + raw_request_body
signature      = hex( hmac_sha256( endpoint_secret, signed_payload ) )
Paayed-Signature: t=1757001600,v1=8d5c3ab1...c41f

Read 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>

Delivery and retries

AttemptDelay after the previous one
1Immediately
21 minute
35 minutes
430 minutes
52 hours
66 hours
712 hours
824 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.

Troubleshooting

Not receiving webhooks

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.

Signature verification failing

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.

Events arriving out of order

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.

Built with Sourcey