Delivery & retries
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 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 resend the same body byte for byte: the payload is captured the moment the event happens, so nothing changes between attempts except the ROASForm-Attempt header and the signature timestamp.
Deduplication and ordering
Two different keys matter:
- Retry duplicates share the same top-level
id: ignore repeats of anidyou've processed. - Business duplicates need
data.submission.id. One completion with a lead fires bothform_submissionandcompleted_leadas separate deliveries with different top-level ids, so subscribe to one of the two unless you deliberately want both, and correlate them onsubmission.id. The same key tiesbooked_callback to its completion, and groups repeatedform_stepdeliveries. Rarely, twoform_stepdeliveries with different ids can even carry identical data: progress that lands while an earlier delivery is already on its way out. Deduplicating onidalone won't catch that; processing each delivery as a snapshot keyed onsubmission.idmakes it a no-op.
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 queues one sample delivery for every event the webhook subscribes to, each shaped exactly like a real delivery of that event, 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.

