> For the complete documentation index, see [llms.txt](https://docs.moderncollections.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.moderncollections.io/webhooks/retries.md).

# Retries

We retry failed webhook deliveries with a fixed backoff schedule. The first attempt happens inline with the event; up to **four** additional retries are scheduled by the delivery worker. After the last failure the delivery moves to the **dead-letter queue** for operator review.

## What counts as failure

| Receiver response                                     | Treated as                                                                                                                                                          |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2xx (200, 201, 202, 204, …)                           | Success                                                                                                                                                             |
| 408 Request Timeout                                   | Transient — retried                                                                                                                                                 |
| 429 Too Many Requests                                 | Transient — retried                                                                                                                                                 |
| Other 4xx (400, 401, 403, 404, …)                     | **Permanent** — straight to dead-letter, no retries                                                                                                                 |
| 5xx                                                   | Transient — retried                                                                                                                                                 |
| Connection error / TLS failure / timeout (> 10s)      | Transient — retried                                                                                                                                                 |
| SSRF guard rejection (URL resolves to a forbidden IP) | On the first send the event is **dropped** — no delivery row is created and nothing is retried. On a retry re-validation failure the delivery moves to dead-letter. |

The 10-second HTTP timeout applies to each attempt.

## Retry schedule

Five total attempts. The first is inline (`attempt 1`); the remaining four are scheduled by the delivery worker after the previous failure.

```
Attempt 1: immediately (inline with the event)
Attempt 2: +1 minute   after attempt 1 failure
Attempt 3: +5 minutes  after attempt 2 failure
Attempt 4: +30 minutes after attempt 3 failure
Attempt 5: +2 hours    after attempt 4 failure
(attempt 5 failure → no further retry; delivery is dead-lettered immediately)
```

After all five attempts fail (or after any permanent 4xx response), the delivery row flips to status `dead_letter` and stops being retried. There is no operator-facing email or in-dashboard surface for this today — recovery happens via platform support.

## Delivery state

Every outbound delivery is persisted in the platform's `outbound_webhook_deliveries` table with one of three statuses:

| Status        | Meaning                                                      |
| ------------- | ------------------------------------------------------------ |
| `pending`     | First attempt in flight, or scheduled for a future retry.    |
| `delivered`   | A 2xx response was received. Terminal.                       |
| `dead_letter` | All retries exhausted or a permanent 4xx response. Terminal. |

The POST body bytes are preserved unchanged across retry attempts — re-serialising the payload would change byte ordering and break HMAC verification. `X-Webhook-Timestamp` is refreshed to the current wall-clock time on each retry, and `X-Signature` is recomputed over that fresh timestamp and the original body bytes. This means a standard 5-minute freshness check will not reject retried deliveries, but `X-Webhook-Timestamp` reflects when the retry was sent, not when the original event occurred.

## Idempotency on your side

The same delivery may arrive **more than once** in edge cases (process crash between POST and our row update; receiver returned 5xx but actually processed the request). Handle this with idempotency at the resource level:

* Key on the underlying resource identity: `(event, placement_id, call_id)` for call events, `(event, placement_id, commitment_id)` for commitment events, `(event, placement_id, dispute_reason, source)` for disputes.
* Or key on `(event, placement_id)` plus a monotonic field from the payload (e.g. `new_status`).
* Store seen keys for at least 24 hours (covers the full retry window).

There is no `X-Delivery-Id` header — the platform's internal delivery UUID is not exposed in the request.

```python
async def receive_webhook(request: Request):
    # ... signature verification ...
    event = await request.json()
    key = f"{event['event']}:{event['placement_id']}:{event.get('new_status', '')}"
    if await redis.set(f"mc:webhook:{key}", "1", nx=True, ex=86400) is None:
        return {"received": True, "duplicate": True}  # already processed
    await enqueue_for_processing(event)
    return {"received": True}
```

## Manual redelivery

There is no self-service redelivery UI today. If you've recovered from a downtime and need replays of dead-lettered deliveries, contact platform support with the affected creditor, placement, and time window — we can re-attempt them for you.

A replay reuses the original signed bytes and timestamp — your freshness check will reject anything older than 5 minutes, so coordinate a re-sign before replay if the gap is wider.

## Ordering

Events are delivered in roughly the order they occurred but **not strictly ordered**. Two events on the same placement may arrive out of order if one fails and gets retried while the next succeeds first.

If ordering matters for your processing:

* Use the per-event monotonic fields where present (e.g. `new_status` after `previous_status`, `attempt_count` on call events).
* Reject events whose state transition you've already applied.

## Receiver SLA recommendations

* **Receiver timeout budget:** acknowledge within our 10-second HTTP timeout. Targeting \~1 second leaves room for retries on your side without hitting our budget.
* **Don't run business logic inline.** Acknowledge fast, enqueue, process async.

## Testing your receiver

There is no in-product "send test event" button today. To exercise your receiver end-to-end in a dev environment, create a placement and walk it through the events you want to receive (e.g. simulate a call, post a commitment) using the development tooling described in the [first API call](/getting-started/first-api-call.md) flow.

## See also

* [Signature verification](/webhooks/signature-verification.md)
* [Error handling](/getting-started/error-handling.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.moderncollections.io/webhooks/retries.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
