# Delivery & retries

> At-least-once delivery, the retry schedule, and response rules.

---
title: Delivery & retries
description: At-least-once delivery, the retry schedule, and response rules.
---

Deliveries are **at-least-once**. Deduplicate on the webhook's `id`, which is stable across retries; the `X-ROASForm-Attempt` header tells you which attempt you are seeing.

## Responding

Respond `2xx` within **10 seconds**, then process asynchronously. Anything slower counts as a timeout and is retried.

| Your response | Result |
| --- | --- |
| `2xx` | Delivered. No retry. |
| `3xx` | Failed permanently. Redirects are never followed. |
| `4xx` except `408`, `429` | Failed permanently. No retry. |
| `408`, `429`, `5xx` | Retried with backoff. |
| Timeout or network error | Retried with backoff. |

## Retry schedule

Each delivery gets up to **5 attempts**. Failed attempts back off exponentially: roughly 1 minute, then 5, then 25, capped at 1 hour, with jitter. Retries keep the same `id` and `createdAtUtc`; the rest of the body is rebuilt with current data at each attempt, so a retry can carry newer information than the original try.

## Deduplication and ordering

Two different keys matter:

- **Retry duplicates** share the same top-level `id` — ignore repeats of an `id` you've processed.
- **Business duplicates** need `data.submission.id`. One completion with a lead fires **both** `form_submission` and `completed_lead` as separate deliveries with *different* top-level ids — subscribe to one of the two unless you deliberately want both, and correlate them on `submission.id`. The same key ties `booked_call` back to its completion, and groups repeated `form_step` deliveries.

Deliveries are **not ordered**: retries with backoff can arrive after newer events. Don't apply updates last-write-wins by arrival — key your records on `submission.id` and order by `createdAtUtc`.

## Test deliveries

The **Send test event** button in the dashboard delivers a sample webhook with placeholder data plus `"sample": true` inside `data`, so your integration can recognize and skip it. Test deliveries are signed like real ones and appear in your webhook's logs.

## Delivery logs

Every attempt is recorded in the dashboard under your webhook's **Logs**: status, HTTP response, timing, and the full request and response bodies, including the retry chain for a delivery.
