What counts as success
A delivery succeeds only if your endpoint returns a2xx within the request
timeout — 10 seconds by default. Anything else, non-2xx or network error, is retried.
Respond fast and queue the real work. A handler that does fulfilment inline is a handler
that times out under load and gets replayed.
The backoff schedule
Exponential, capped:
These are server-configurable, so treat the exact numbers as current defaults rather
than a contract. The shape — fast at first, then hours apart, for days — is the part to
design against.
Exhaustion
After the last attempt the delivery is markedexhausted and is not retried
automatically. The store’s owners are emailed, at most once per webhook per 24 hours, so
a broken endpoint produces one alert rather than a flood.
Once your endpoint is healthy again, replay exhausted deliveries — from the dashboard
(owner, admin or developer), or through the resend endpoint.
Resending an exhausted delivery buys exactly one more attempt. The attempt
counter is not reset: the delivery is pushed back into the queue at its existing
count, one request is made, and if that request does not return a
2xx the delivery
is immediately exhausted again. It does not restart the multi-day retry
schedule.So fix the endpoint first, verify it, and only then resend. If the resend fails you
are back where you started and must resend again.At-least-once delivery
The same event can arrive more than once — an automatic retry after your endpoint was slow but actually succeeded, or a manual resend. Your handler must be idempotent. Idempotency has to live in your business state. Before acting, ask whether the state change has already been applied — is this order already fulfilled? Look updata.id,
or your own data.extraContext.purchaseId / data.metadata.purchaseId, and no-op if the
work is done. That check is correct whether the duplicate came from a retry, a resend, or
two workers racing.
X-Taberna-Delivery-Id is still worth recording — for correlation with the dashboard,
and as a cheap short-circuit for automatic retries piling onto an expensive handler — but
it is not sufficient on its own. A handler that records the id and then crashes
before applying the change has poisoned itself: the resend that would have fixed it gets
skipped as a duplicate.
An idempotent handler
Verify, parse, then apply the change against your own state — never gate on the delivery id.purchase.id in whatever
table records the fulfilment, so two concurrent deliveries cannot both pass the
fulfilledAt check.
List deliveries
GET /v1/webhook-deliveries — requires the webhooks:read scope. Newest first.
200 OK
lastResponseStatus is the HTTP status your endpoint last returned, or null if the
attempt failed at the network level. Exactly one of orderId / invoiceId is set. A
store with no webhook configured returns an empty page, not an error.
Resend a delivery
POST /v1/webhook-deliveries/{deliveryId}/resend — requires the webhooks:write
scope. Returns 204 with no body.
webhooks:write covers replay only. It does not permit changing where a store’s
events are sent — repointing the webhook URL stays a dashboard action, deliberately.Current limitations
One webhook URL per store. Every event for the store goes to that single endpoint. If you need to fan out to several consumers, receive once and dispatch yourself. No per-event subscription filtering. You cannot subscribe toorder.completed alone;
you receive all ten event types and filter on event in your handler.
Both are listed in What Taberna does not
do.