Skip to content

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.

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.

{
"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.

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.

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).

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.

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 …/{id}/rotate-secret New secret; the old one keeps signing for 24 hours
GET …/{id}/deliveries The 50 newest attempts, a bare array sorted by createdAt then id. No payload
POST …/{id}/deliveries/{deliveryId}/replay Re-fire one, with a fresh retry ladder
POST …/{id}/deliveries/replay 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?