Webhooks
We tell you when a payout changes state, so you do not have to poll. Webhooks are the nudge; the API is the truth. Build reconciliation against GET /payments/organizations/{orgId}/orders/{payoutId} and treat a delivery as a reason to look, never as the record itself.
Signing: Standard Webhooks#
We sign with Standard Webhooks, the same scheme Svix popularised — so you can verify with an off-the-shelf library in your language instead of hand-rolling against a format only we speak.
Three headers arrive with every delivery:
| Header | Meaning |
|---|---|
svix-id |
The delivery id. Idempotency key — dedupe on this. |
svix-timestamp |
Unix seconds. Reject anything far from your own clock. |
svix-signature |
v1,<base64> — a space-separated list, so a rotation can ship two valid signatures on one request |
The signed content is the delivery id, the timestamp and the raw body, joined by dots:
signedContent = `${svix-id}.${svix-timestamp}.${rawBody}`
signature = base64(HMAC_SHA256(base64decode(secret minus "whsec_"), signedContent))Verify over the bytes you received, before any JSON parsing. Re-serializing the body changes it — a reordered key or a different float formatting produces a signature mismatch on a delivery that was perfectly valid.
Your secret is issued with the endpoint, in the conventional whsec_ form. It is shown once.
Events#
Six event types, and they are the canonical payout states:
| Event | Meaning | Terminal |
|---|---|---|
payout.pending |
Accepted, not yet moving | |
payout.processing |
On its way | |
payout.completed |
The beneficiary has been paid; a later bank return is still possible | |
payout.failed |
It failed; inspect fundsReturned before changing your ledger |
✓ |
payout.returned |
It settled and the receiving bank later returned it | ✓ |
payout.canceled |
Stopped before it moved | ✓ |
The rail is never named. No provider's own status words appear in any payload, which is what lets us re-route your organization to a different payment network without your ledger changing. If you find yourself branching on something rail-shaped, you are reading a field you should not depend on.
New event types are a backwards-compatible change. Ignore ones you do not recognize rather than throwing — see Versioning.
Delivery#
We consider a delivery successful on any 2xx. Anything else — including a timeout — is retried on a fixed ladder:
| Attempt | Sent |
|---|---|
| 1 | Immediately |
| 2 | +1 minute |
| 3 | +5 minutes |
| 4 | +30 minutes |
| 5 | +2 hours |
| 6 | +6 hours |
Six attempts over roughly 8.6 hours, after which the delivery is left failed and we stop. It is not lost: it stays in the delivery log, and you can replay it.
Return 2xx fast, and do the work afterwards. A receiver that finishes its own processing before responding is a receiver that times out under load and gets retried, which is how one slow database write becomes a duplicate.
Ordering, duplicates, and the two rules that follow#
Deliveries are at-least-once and not ordered. Both are consequences of retrying, and both are ordinary; every webhook system worth trusting has them.
- Dedupe on
svix-id. The same event may arrive twice — a retry after a
timeout that actually succeeded is the common case.
- Never move a payout backwards. A delayed
payout.processingcan land
after payout.completed; ignore it. The one forward transition from completed is payout.returned, whose body carries status: failed and failureCode: returned_by_bank. Only failed, returned, and canceled are terminal.
Managing endpoints#
GET /organizations/{orgId}/webhook-endpoints |
List |
POST /organizations/{orgId}/webhook-endpoints |
Register; returns the signing secret once |
DELETE /organizations/{orgId}/webhook-endpoints/{id} |
Remove |
POST /organizations/{orgId}/webhook-endpoints/{id}/enabled |
Pause or resume without losing the secret |
GET /organizations/{orgId}/webhook-endpoints/{id}/deliveries |
Recent attempts, with status and next retry |
POST /organizations/{orgId}/webhook-endpoints/{id}/deliveries/{deliveryId}/replay |
Re-fire one, with a fresh retry ladder |
The delivery log is there for the integration you are debugging at 2am: it shows what we sent, what came back, how many attempts remain and when the next one is due. Reach for it before you reach for us.
These management routes use a human dashboard session and require a signer role. Partner API keys can receive and verify deliveries, but cannot create, pause, replay, or repoint live webhook endpoints.
Before you go live#
- You verify signatures over the raw bytes, and reject on mismatch.
- You dedupe on
svix-id. - You ignore events older than the state you hold.
- You return
2xxbefore doing your own work. - Your endpoint is reachable from the public internet over HTTPS.
You reconcile against the API on a schedule anyway, because a webhook you never received is indistinguishable from one that never fired.