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

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.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",
  "data": {
    "object": { "id": "pay_01JBQ7X2F9KDNW3M8T", "object": "payment", "status": "succeeded", "amount": 42.50 },
    "previous_attributes": { "status": "authorized" }
  }
}

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-serialised 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
1Immediate
21 minute
35 minutes
42 hours
56 hours
612 hours
7–824 hours

Roughly 48 hours of cover, with jitter so a recovering endpoint isn't hit with every backlogged delivery at once. After 24 hours of continuous failure the endpoint is marked degraded; after 7 days it's disabled and every administrator is emailed. Re-enabling offers a replay of everything missed within the 30-day retention window.

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-serialised version. Confirm you're using the endpoint's own signing secret, not your API key.

Events arriving out of order

Ordering isn't guaranteed. Use created_at and the event id to detect and discard anything stale.