Webhooks
Avvio sends your server a signed event each time a payout changes state. Each
delivery carries the same id as its row in
GET /payments/organizations/{orgId}/events, the feed you reconcile from. A
delivery can go missing; the feed can’t.
Receive and verify webhooks builds a receiver step by
step, and Webhook events shows every event type
with a full body.
Signing
Section titled “Signing”Deliveries are signed with Standard Webhooks, the scheme Svix popularized, so an off-the-shelf library can verify them.
| Header | Meaning |
|---|---|
svix-id |
The event id. Stable across retries and equal to the body’s id. Dedupe on it |
svix-timestamp |
Unix seconds. Reject a delivery more than five minutes from your clock |
svix-signature |
Space-separated v1,<base64> values. A rotation sends two, so accept a match on any |
Avvio-Webhook-Version |
The shape of data, equal to the body’s apiVersion (1 today). Branch on it before you parse |
signedContent = `${svix-id}.${svix-timestamp}.${rawBody}`signature = base64(HMAC_SHA256(base64decode(secret minus "whsec_"), signedContent))The secret has the whsec_ prefix and is shown once at creation and once at
each rotation. Compare signatures in constant time.
The body
Section titled “The body”{ "id": "cmf3k2xb20002q8b7c1s9m4rw", // opaque; equals svix-id and the feed row's id "sequence": "48213", // the feed cursor for this event "type": "payout.completed", "createdAt": "2026-09-03T10:00:00.000Z", "apiVersion": 1, // equals Avvio-Webhook-Version "livemode": true, // false from a sandbox organization "data": { … } // the event payload, verbatim}data is built once, when the event is recorded. The webhook and the feed
read that stored object, so both give the same numbers. For payout.* it is
the flat payout (schema WebhookPayout); checkout events use
WebhookCheckoutPayment. Money is decimal strings beside a currency field,
except fee, which is a { amount, currency } object.
Events
Section titled “Events”Payout, batch, approval and endpoint events go to every endpoint by default.
Checkout-payment events are opt-in: list them in events when you register,
or filter the feed with type=. They are opt-in because a payout receiver that
answers 4xx to an unknown type climbs toward auto-disable. Answer 2xx to
any type you don’t handle, and never throw on a new one: new types are a
backwards-compatible change.
A newly registered endpoint can also receive events recorded up to about 10
minutes before it was created. Dedupe on id and ignore a status older than
the one you hold, and these are harmless.
Every state change produces one event, written in the same database transaction as the change. A dispatcher runs every 15 seconds, so a delivery lands within about 15 seconds and close transitions arrive as one burst.
One event per transition is not one event per state. A payout can skip
payout.processing, in the sandbox too (scenario 0002), so handle any
forward edge from the state you hold (State transitions).
Batches have no in-progress event; each created line emits its own
payout.* events. payout_approval.executed carries the payoutId that
joins a 202 to its payout.
Two things are not announced: a bank deposit held short of the total (it stays
pending until accepted in the dashboard), and a dispute before it resolves.
No payload names the payment network or uses its status words, so a re-route
doesn’t change your ledger.
Delivery and retries
Section titled “Delivery and retries”Any 2xx is a success. Anything else, including a timeout, is retried, each
step jittered by ±20%:
| Retry | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| After the previous attempt | 1 min | 5 min | 15 min | 1 h | 3 h | 6 h | 12 h | 24 h | 24 h |
Nine retries cover about 70 hours. After that the delivery is dead: it stays in
the delivery log with its lastError and is never retried automatically or
deleted. The event stays in the feed.
Deliveries are at least once and offered in sequence order per endpoint, but
retries can reorder them. Dedupe on svix-id, and never move a payout
backwards: a late payout.processing can land after payout.completed. The
one forward move from completed is payout.returned, whose body carries
status: failed and failureCode: returned_by_bank
(the bank return).
Auto-disable
Section titled “Auto-disable”After three dead deliveries and five days with no accepted delivery, the
endpoint is disabled with disabledReason: auto_disabled_after_failures. Your
organization’s owners and admins are emailed and webhook_endpoint.disabled goes to your
other endpoints. Events keep being recorded but nothing is sent there until
someone re-enables it. The endpoint list shows consecutiveFailures,
lastSuccessAt and lastFailureAt, so you can see it coming.
Managing endpoints
Section titled “Managing endpoints”Paths below start at /organizations/{orgId}/webhook-endpoints.
| Endpoint | What it does |
|---|---|
GET … |
List, with health |
POST … |
Register; returns the signing secret once |
DELETE …/{id} |
Remove |
POST …/{id}/enabled |
Pause or resume without losing the secret; resuming clears health |
POST …/ |
New secret; the old one keeps signing for 24 hours |
GET …/ |
The 50 newest attempts, a bare array sorted by createdAt then id. No payload |
POST …/ |
Re-fire one, with a fresh retry ladder |
POST …/ |
Re-fire deliveries in a from/to window, up to 1,000 |
Listing endpoints and reading deliveries accept an API key. Everything else is
dashboard-only, for owner, admin and operator members, so a leaked key
can see where deliveries go but can’t repoint them. A test key can register an
endpoint and read its deliveries under
/payments/organizations/{orgId}/sandbox/webhook-endpoints, to rehearse
verification without the dashboard.
Rotation returns the new whsec_ once, with previousValidUntil. Until then
every delivery carries two signatures, so deploy the new secret at your own
pace and accept either.
A replay keeps the event id, so your dedupe still recognizes it. Bulk replay
takes ISO-8601 from and to, and status: failed (the default, dead
deliveries only) or all. It runs oldest first and answers { requeued }.
Use it to catch up after re-enabling a disabled endpoint.
Was this page helpful?