# Retries and delivery

> Timeouts, the retry schedule, what counts as success, ordering, and how to debug failing deliveries.

Source: https://dev.ody.co/webhooks/retries

## What counts as success

Any `2xx` response within **10 seconds**. Anything else counts as a failure and is retried: a `3xx` (redirects aren't followed), `4xx`, `5xx`, a timeout, a TLS error or a refused connection.

## Retry schedule

Each event gets up to **three attempts** per endpoint:

| Attempt | When |
|---|---|
| 1 | Immediately |
| 2 | About 5 seconds after attempt 1 |
| 3 | About 30 seconds after attempt 2 |

Every attempt is signed fresh, with a new `webhook-timestamp`, and keeps the same `webhook-id`. Once the third attempt fails, the delivery is marked `failed` and not tried again.

> **Plan for missed events:**
The retry window is short (well under a minute), so an endpoint that's down for a deploy can miss events. Make your integration **self-healing**: periodically reconcile against the API (for example, page through [`GET /conversations`](https://dev.ody.co/api/conversations#list-conversations) ordered by `lastActivityAt`), and treat webhooks as a fast path rather than the only source of truth. A longer retry window is on the roadmap.

Endpoints are never disabled automatically. To pause deliveries, set `status: "disabled"` with [`PATCH /webhooks/:id`](https://dev.ody.co/api/webhook-endpoints#update-an-endpoint).

## Ordering and duplicates

- Events for different endpoints, and successive events for one endpoint, are sent **concurrently**. Arrival order isn't guaranteed; a `call.summary.completed` can arrive before its `call.completed`.
- The same event can arrive **more than once** (for example, your server processed it but the response timed out). Store processed `id`s, for at least a day, and skip repeats.

## Debugging

| Tool | What it shows |
|---|---|
| [`GET /webhooks/:id/deliveries`](https://dev.ody.co/api/webhook-endpoints#list-deliveries) | Every delivery and each attempt's `statusCode` or `error` and `durationMs` |
| [`POST /webhooks/:id/test`](https://dev.ody.co/api/webhook-endpoints#send-a-test-delivery) | One signed `ping` with the result inline. No retries |

Delivery with a failed first attempt:

```json
{
  "id": "d7e1…",
  "webhookId": "3a8f…",
  "eventId": "evt_4f9b2c0d8e1a4b7c9d2e3f4a5b6c7d8e",
  "status": "success",
  "attempts": [
    { "at": "2026-10-14T18:05:42.400Z", "statusCode": 503, "durationMs": 88 },
    { "at": "2026-10-14T18:05:47.493Z", "statusCode": 200, "durationMs": 121 }
  ],
  "createdAt": "2026-10-14T18:05:47.620Z"
}
```

### Common failures

| Symptom | Likely cause |
|---|---|
| `error: "The operation was aborted due to timeout"` | Your handler takes over 10s. Return `200` first, then process |
| `statusCode: 401` or `403` | Your signature check is failing: raw body, wrong secret, or the secret was rotated |
| `statusCode: 404` or `405` | Wrong path, or the route doesn't accept `POST` |
| `statusCode: 301` or `308` | Redirects aren't followed. Register the final URL |
| `error` mentioning a certificate | Invalid or self-signed TLS certificate |
| No deliveries at all | Endpoint `disabled`, or not subscribed to that type. Check what each type needs in [Event types](https://dev.ody.co/webhooks/events): for example `contact.updated` doesn't fire for CSV imports, and `call.ringing` is inbound-only |
