> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xarchive.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Delivery and retries

> Find out why deliveries stopped, then retries, auto-disable, the delivery log, redelivery, secret rotation, paused rules, and re-testing a fixed endpoint.

Delivery is at least once. A delivery your receiver does not accept is retried for 24 hours, and every delivery has an entry in its endpoint's delivery log that shows its latest attempt. A receiver that keeps failing is disabled, and a rule that runs past its plan's daily allowance is paused; both show it in their `status`.

## Find out why deliveries stopped

Check these in order. Each answer is one read call, a field in the dashboard, or an MCP tool.

1. **Is the endpoint active?** `GET /v1/webhooks/endpoints` returns each endpoint's `status`. `auto_disabled` means its receiver failed for 6 hours or more and nothing is being sent to it: fix the receiver, then [re-test it](#re-test-a-repaired-endpoint). The dashboard shows the same as **Auto disabled** on the endpoint.
2. **Is the rule switched on?** `GET /v1/webhooks/subscriptions` returns each rule's `enabled`. `false` means the rule was switched off on your account, and it stays off until it is switched back on: `PATCH /v1/webhooks/subscriptions/{id}` with `{"enabled": true}`, the rule's switch on the dashboard's **Subscriptions** tab, or `update_webhook_subscription` over MCP. A preview does not switch a rule on.
3. **Is the rule paused?** The same response shows `status` `auto_paused` when the engine has stopped the rule, with a `pause_message` that says why and what clears it. See [Paused rules](#paused-rules).
4. **What did the latest attempt get back?** The [delivery log](#the-delivery-log) shows each delivery's `state`, `attempts`, `last_status_code` and `last_error`. The common receiver failures are below.
5. **Has a matching delivery been queued yet?** If the endpoint is active, the rule is on and active, and the log has no new entry, no matching delivery has been queued yet, which is not the same as nothing having happened. Check how quickly the event type [arrives](/webhooks/events#how-fast-events-arrive) and whether its source is healthy on [data quality](/data-quality), then run the [estimate or the dry-run](/webhooks/estimate-and-dry-run) on the same configuration, where the event type supports it, to see how often it would have fired. Check too that symbols are in the [venue's own form](/webhooks/rules#symbol-conventions).

Common receiver failures in the log:

* **`last_status_code` 400 or 401 from your own signature check.** The body was usually parsed before it was checked, or the secret is not this endpoint's. Check the raw bytes with the whole `whsec_...` secret as the key; see [Signatures and payloads](/webhooks/signatures-and-payloads#verifying-signatures).
* **`last_error` is a timeout.** The receiver did the work before answering. Record the event, answer with a 2xx, then process it off the request, as in [this receiver](/webhooks/signatures-and-payloads#a-receiver-that-does-not-lose-events).
* **`last_status_code` 301, 302, 307 or 308.** The URL redirects, and redirects are not followed. Register the final URL.
* **A create or test call answers 400 naming a plan.** The plan does not include webhook delivery; see [Limits and plans](/webhooks/limits-and-plans).
* **A test or redelivery answers 409.** The account is at today's delivery allowance, which resets at 00:00 UTC. A redelivery also answers 409 while its endpoint is disabled, and the message names the enable route.

## Retries

A delivery succeeds when your receiver answers with a 2xx within 10 seconds. A non-2xx response, a redirect, a timeout or a connection error schedules another attempt on this ladder: 5 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, then hourly. When a response carries `Retry-After`, the next attempt waits that long instead, between 1 second and 5 minutes. Attempts continue for 24 hours from when the delivery was queued; the first attempt that fails after that marks it `exhausted`.

Every attempt is signed again with a fresh `t`, and every attempt carries the same event `id`. Removing duplicates by `id` stops most repeats, but a receiver can still see an event more than once, for example when it stops after acting on an event and before recording that it did, so the effects of handling an event must be idempotent.

## When an endpoint is disabled

An endpoint that keeps failing is disabled automatically: 10 or more consecutive failed attempts, across all of its deliveries, over 6 hours or more. Any 2xx resets the count, and a `429` answer is retried without counting toward it. The endpoint's `status` becomes `auto_disabled`, and it stops receiving deliveries.

Nothing is buffered while it is off. Rules that point at it produce no deliveries, and deliveries that were still waiting for a retry are marked `exhausted` without another attempt. Re-enabling the endpoint is the first step of [re-testing it](#re-test-a-repaired-endpoint).

A disabled endpoint is a receiver problem. A [paused rule](#paused-rules) is a plan or allowance problem. They are separate states with separate fixes, and re-enabling one never resumes the other.

## The delivery log

`GET /v1/webhooks/endpoints/{id}/deliveries` lists an endpoint's deliveries, newest first. `?limit=` sets how many come back: 50 by default, up to 200. Deliveries are kept for 30 days. In the dashboard, the **Deliveries** tab shows the same log across all of your endpoints.

Each delivery has one entry, and it shows that delivery's latest attempt, not one row per attempt:

* `state`: `pending` until the delivery succeeds, including between retries; then `delivered`, or `exhausted` when its retry window closed.
* `attempts`: attempts made so far. `pending` with `attempts` at 0 has not been attempted yet; `pending` with `attempts` above 0 is waiting for a retry, and `last_status_code` and `last_error` say why the last one failed.
* `last_status_code`, `last_error`, `last_latency_ms`: what the latest attempt got back, and how long it took.
* `next_attempt_at`, `delivered_at`, `created_at`: when the next attempt is due, when it succeeded, and when it was queued.
* `event_id`, `event_type`, `payload`: the event, and the full payload that was signed.

## Send a delivery again

`POST /v1/webhooks/deliveries/{id}/redeliver` queues a past delivery for another attempt. It is the same delivery: the delivery id and the event `id` do not change, so a receiver that already processed the event can dedupe it. The retry window and the attempt counter restart, and the recorded `last_status_code`, `last_error`, timing and `delivered_at` are cleared, so the log entry then describes the new attempt. If you need the original failure's details, copy them from the log before you redeliver. In the dashboard, the **Deliveries** tab has **Redeliver** on each row.

A redelivery is a real signed delivery and counts toward the day's delivery allowance. It is refused with a 409 while the endpoint is disabled (the message names the enable route) or while the account is at its daily allowance, and with a 400 on a plan without webhook delivery.

## Rotate a secret

`POST /v1/webhooks/endpoints/{id}/rotate` returns a new secret, once, and keeps the previous secret valid for 24 hours. During that window every delivery carries two `v1` digests in `0xa-signature`, the new secret's first, so a receiver still holding the old secret keeps working and a receiver that has rolled works with the new one. Deliveries sent after the rotation, retries of older ones included, are signed with the new secret.

Only one previous secret is kept. Rotating again inside the 24 hours makes the original secret stop working immediately, so rotate once, deploy the new secret, send a test delivery, and only then rotate again if you need to.

## Paused rules

A rule pauses for one of two reasons, and says which.

### At the daily allowance

When an account goes past its deliveries per day, the rule that crossed the line is paused and reports that it is paused. Because the allowance is counted per account, every other rule on the account pauses the same way on its next match until the allowance resets. The pause is visible on the rule, so a quiet receiver can be told apart from a quiet market.

A paused rule delivers nothing and buffers nothing. While it is paused it records what it is missing: how many occurrences matched, and the window they fall in. The allowance comes back at 00:00 UTC, but a paused rule does not restart with it, so a receiver you have not fixed or an allowance you have not raised cannot be flooded the moment the day rolls over. A rule that keeps pausing is a sizing signal before it is a plan signal: run the [estimate](/webhooks/estimate-and-dry-run) again and move the threshold to a rate that fits your allowance.

### When the plan stops including delivery

When the plan on the account stops including webhook delivery, each rule pauses the next time it matches something, and says that the delivery was refused because the plan does not include webhooks. Two ordinary situations land here: a downgrade to Free, and a payment that is still settling, when for a short window an account can still read as Free while the plan change catches up.

Once the plan includes webhook delivery again, the engine clears this pause by itself, so an upgrade you have just paid for starts delivering without resuming every rule by hand. A daily-allowance pause never lifts itself.

### Reading a pause

A paused rule says so on `GET /v1/webhooks/subscriptions`, whichever reason paused it. `GET /v1/webhooks/limits` also counts paused rules under `paused_subscriptions`.

* `status`: `active` while the rule is serving, `auto_paused` while the engine has it stopped.
* `pause_message`: why this rule is paused and what clears it, in plain words. Present only while it is paused. Show this one to a person.
* `pause_reason`: the same cause as a stable value, `deliveries_per_day_cap` or `plan_no_webhooks`. Use it for branching, not for display.
* `paused_at`: when the current gap started.
* `suppressed_count`, `suppressed_first_at`, `suppressed_last_at`: how many matches were observed and not delivered since the pause began, and the window they fall in. The count is a floor rather than a total.
* `last_paused_at`, `last_resumed_at`, `last_pause_reason`, `last_suppressed_count`, `last_suppressed_first_at`, `last_suppressed_last_at`: the same record for the previous pause, kept through a resume so a missed window can still be explained later.

### Resuming

Resuming is an explicit call: `POST /v1/webhooks/subscriptions/{id}/resume` for one rule, or `POST /v1/webhooks/subscriptions/resume` for every paused rule at once, which is usually the one you want because the allowance is counted per account. The dashboard has **Resume** on each paused rule and **Resume all** for the account. A resume is refused with a 409 while the account is still at its daily allowance, and with a 400 on a plan without webhook delivery, because the rule would pause again on its next match. It leaves your own on and off switch alone, so a rule you had switched off stays off.

A resume replays nothing, because nothing was buffered. It returns the gap it just closed, with `replay_window` as the window to read back yourself and a `note` that says what can be recovered:

* **Market-wide liquidation, price, funding and open interest events** can be read back from the REST market routes for the same venue, symbols and window. A delivered payload names its own route in `api_url`, so an earlier delivery from the same rule shows the shape of the call.
* **Platform events** (billing, ingest health, archive gaps, chain upgrades, listings and exports) cannot be reproduced from the archive, so that part of the window is lost.
* **Wallet events.** Liquidations of a watched address are in `GET /v1/hyperliquid/liquidations/user/{address}`, and spot TWAP changes are in `GET /v1/hyperliquid/spot/twap/user/{address}`. Fills, transfers, order rejections, HIP-4 settlements, and TWAP changes on perps and HIP-3 have no per-address route. A paused wallet rule usually stops being scanned at all, so its count comes back as `null` rather than zero: nothing was looked at, which is not the same as nothing happening.

Full field lists are in the [subscription schema](/schemas/components/webhook-subscription) and the [resume gap schema](/schemas/components/webhook-resume-gap).

## Re-test a repaired endpoint

Once the receiver is fixed:

1. **Re-enable the endpoint** if it was disabled: `POST /v1/webhooks/endpoints/{id}/enable`, **Enable** on the endpoint in the dashboard, or `enable_webhook_endpoint` over MCP. This also resets its failure count.
2. **Send a test delivery**: `POST /v1/webhooks/endpoints/{id}/test`, **Send test event**, or `test_webhook_endpoint`. Keep the `delivery_id` it returns.
3. **Check the result**: find that `delivery_id` in `GET /v1/webhooks/endpoints/{id}/deliveries`. It is done when `state` is `delivered` and `last_status_code` is your receiver's 2xx.
4. **Redeliver previously queued deliveries**: redeliver the deliveries that ended `exhausted` while the receiver was down, using their ids from the same log. Each one counts toward the day's allowance. Events that matched while the endpoint was disabled were never queued, so they are not in the log to redeliver.

```bash theme={"theme":"github-dark"}
curl -X POST "https://api.0xarchive.io/v1/webhooks/endpoints/$ENDPOINT_ID/enable" -H "X-API-Key: $OXARCHIVE_API_KEY"
DELIVERY_ID=$(curl -s -X POST "https://api.0xarchive.io/v1/webhooks/endpoints/$ENDPOINT_ID/test" \
  -H "X-API-Key: $OXARCHIVE_API_KEY" | jq -r '.data.delivery_id')
curl -s "https://api.0xarchive.io/v1/webhooks/endpoints/$ENDPOINT_ID/deliveries?limit=20" \
  -H "X-API-Key: $OXARCHIVE_API_KEY" \
  | jq --arg id "$DELIVERY_ID" '.data[] | select(.id == $id) | {state, attempts, last_status_code, last_error}'
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.