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

# Delivery and retries

> What counts as a successful delivery, how Pazy retries failures, and how to handle duplicates and ordering

## What counts as success

Pazy treats a delivery as successful only when your endpoint returns a **`2xx`** status within the timeout. Everything else is a failure:

* Any `3xx`, `4xx` or `5xx` status. Redirects are not followed.
* No response within the timeout. The default is 10 seconds and the maximum is 30.
* A connection, DNS or TLS error.
* A response body larger than 64 KB.
* A URL that fails the [URL requirements](/webhooks/setup#url-requirements).

Return `200` or `204` with an empty body as soon as you've stored the event.

## Retry schedule

A failed delivery is retried with exponential backoff and about ±10% random jitter, for up to **5 attempts** in total.

| Attempt | Sent | Header value |
| - | - | - |
| 1 | Immediately when the event occurs | `X-Pazy-Webhook-Attempt: 1` |
| 2 | About 30 seconds after attempt 1 fails | `2` |
| 3 | About 60 seconds after attempt 2 fails | `3` |
| 4 | About 2 minutes after attempt 3 fails | `4` |
| 5 | About 4 minutes after attempt 4 fails | `5` |

Retries are picked up by a scheduler that polls every 30 seconds by default, so an attempt can start somewhat later than the nominal delay.

After the fifth failed attempt, Pazy stops. The delivery is marked as exhausted and is not sent again automatically.

<Warning>
  A self-serve view of failed deliveries and manual redelivery are coming soon. Until then, if your endpoint was down for more than about 8 minutes, reconcile with the API: list recent records and compare them with what you received. If you need missed events replayed, contact [customer-support@pazy.io](mailto:customer-support@pazy.io).
</Warning>

## Handle duplicates

Delivery is **at-least-once**. The same event can reach you more than once, for example when your `200` is lost on the network and Pazy retries. The event `id` is the same on every attempt.

1. Before processing, look up `id` in a table or cache of handled events.
2. If it's there, return `200` and do nothing else.
3. If not, process the event and record the `id`. Do both in one database transaction where you can.

For `payment.*` events, you can also dedupe on `data.paymentId`, which is unique per payment.

## Don't rely on ordering

Events can arrive out of order. A retry of an older event can land after a newer one has already been delivered. For example, you might see `expense.approval.status_changed` for `APPROVED` before an earlier retry for `PENDING`.

* Compare timestamps to decide which event is newer: the envelope's `createdAt`, or `paymentDate` on `payment.*` events (it is `null` on failures and reversals).
* Treat status fields as the latest known state only if the event is newer than what you already hold.
* When correctness matters, fetch the current record by `id` with the API.

## Respond fast, process later

Your handler holds up Pazy's delivery while it runs, and a slow response risks a timeout and a duplicate.

<Steps>
  <Step title="Verify">
    Check the [signature](/webhooks/verify-signatures).
  </Step>

  <Step title="Persist">
    Save the raw event, or push it onto a queue such as SQS, RabbitMQ or a jobs table.
  </Step>

  <Step title="Acknowledge">
    Return `200` right away.
  </Step>

  <Step title="Process">
    A background worker does the real work, and retries on its own failures.
  </Step>
</Steps>

## Return the right status

| Situation | Respond with |
| - | - |
| Signature invalid or timestamp stale | `401` |
| Event accepted, or a duplicate you already handled | `200` |
| Event type you don't handle | `200`, so Pazy doesn't keep retrying it |
| Temporary problem on your side, such as a database outage | `500` or `503`, so Pazy retries |

<Note>
  A `4xx` response is also retried. Pazy doesn't treat any non-`2xx` status as permanent, so don't return `400` for events you simply don't care about.
</Note>

## Differences from API retries

The retry rules in [Errors and retries](/guides/errors-and-retries) apply to requests **you** send to Pazy. Webhook retries are Pazy retrying requests to **you**, on the fixed schedule above. They don't honor `Retry-After` headers.


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