> For the full page index, fetch https://echonova.ai/llms.txt

# Delivery, retries and failures

What counts as a successful delivery, the retry schedule, the delivery status values, when a webhook is disabled, and how to handle duplicates.

This page describes what happens after an event fires: how long a delivery
waits for your endpoint, when it retries, and when it gives up. Use it to
size your handler's timeout and to design de-duplication.

## One delivery

When an event fires, every enabled webhook subscribed to it receives one
delivery. Each attempt does the following:

| Step  | Detail                                                                                                                                |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Sign  | A fresh Unix timestamp is taken and the body is signed. See [Verify a webhook signature](/docs/developers/webhooks-verify-signature). |
| Send  | One `POST` to your **Endpoint URL** with `Content-Type: application/json` and the three `X-Webhook-*` headers.                        |
| Wait  | Up to 10 seconds for your response. The request is aborted after that.                                                                |
| Judge | Any status from `200` to `299` is a success. Any other status, a timeout, a connection error or a TLS error is a failure.             |

Only the status code of your response matters; the body is not
interpreted.

## Retry schedule

A delivery gets 3 attempts in total. The wait before each retry doubles,
starting at 5 seconds.

| Attempt | Wait before it | Earliest start | Latest start |
| ------- | -------------- | -------------- | ------------ |
| 1       | none           | 0 s            | 0 s          |
| 2       | 5 s            | 5 s            | 15 s         |
| 3       | 10 s           | 15 s           | 35 s         |

"Earliest" assumes each failed attempt returns at once, for example with a
`500`. "Latest" assumes each attempt runs into the 10-second timeout. In the
worst case the last attempt ends about 45 seconds after the first began.
Allow a little on top of these figures.

Retries send the same body. Only the timestamp and the signature change. A
delivery is final once an attempt gets a `2xx`, or after the third failed
attempt.

## Where to see deliveries

Delivery outcomes are not shown in the portal. The **Webhooks** tab lists
each webhook's name, events, id, endpoint and status. Log every delivery on
your side, including the headers, so you can investigate from your own
records.

## Automatic disable

The count is of consecutive failed deliveries per webhook, not of attempts.
A delivery counts as failed after all 3 attempts.

| Event                       | Effect on the counter                                   |
| --------------------------- | ------------------------------------------------------- |
| A delivery ends `failed`    | Counter goes up by 1.                                   |
| A delivery ends `delivered` | Counter resets to 0.                                    |
| The counter reaches 5       | The webhook is disabled. The portal shows **Disabled**. |
| You enable the webhook      | Counter resets to 0.                                    |

A disabled webhook is skipped when events fire; the events are not sent
later when you enable it. Saving the **Edit Webhook** dialog with
**Enable** on also resets the counter.

## Handling duplicates

Two things can make your endpoint see the same event twice: a retry after
your server processed the event but did not answer within 10 seconds, and
`spending.exhausted`, which fires on every metered call that leaves a key at
or above its limit. The body carries no delivery id, so de-duplicate on the
content:

| Event                         | De-duplication key                                                                                                                |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `job.completed`, `job.failed` | `jobId` plus the `X-Webhook-Event` header. A job finishes once, so a second delivery with the same pair is a retry.               |
| `spending.warning`            | `apiKeyId` plus the event name. It is sent once per billing cycle.                                                                |
| `spending.exhausted`          | `apiKeyId` plus the event name. Treat it as a level, not an edge: mark the key exhausted and ignore repeats until the next cycle. |

Store the key before you do the work, in a store shared by all instances of
your handler, and make the work itself safe to run twice. A hash of the raw
body also identifies a retry, because retries reuse the body byte for byte.

Answer `2xx` first, then process. If processing can take more than a few
seconds, queue it and return at once. That keeps you under the 10-second
limit and avoids the retry that causes most duplicates.

**Recommended**

- Verify the signature, record the de-duplication key, return `200`, then
  download the audio or update your records from a worker.

**Less effective**

- Download the audio, transcode it and write three tables, then return
  `200` twelve seconds later, so the delivery is retried and processed
  twice.

## FAQ

### How many times will you call my endpoint for one event?

Up to 3 times, about 5 and then 10 seconds apart, until one attempt gets a
`2xx`. Each attempt waits up to 10 seconds for your response.

### Does a slow 200 count as delivered?

No. A response that arrives after 10 seconds is a failed attempt, even if
it is a `200`, and the delivery is retried.

### Are missed events replayed when I re-enable a webhook?

No. Events that fired while the webhook was disabled are not sent later.
Poll the job endpoints for anything that ended during that time.

### Why did my webhook turn itself off?

Five deliveries in a row failed after all their attempts. Fix the endpoint,
then enable the webhook again; the counter starts from zero.

### Can I see past deliveries?

Not in the portal. Keep your own log of every delivery, with its headers,
on the receiving side.

## Related

- [Configure a webhook](/docs/developers/webhooks-configure)
- [Webhook events](/docs/developers/webhooks-events)
- [Verify a webhook signature](/docs/developers/webhooks-verify-signature)
