# Retries and replay

> How Maxmove retries failed webhook deliveries, how to handle duplicates and out-of-order events, and how to replay failed events.

## Answer quickly

Answer every webhook request with a `2xx` status within 5 seconds, and process the event afterwards, for example from a queue. A slow answer counts as a failure and is retried.

## Retries

| Your endpoint answers | What Maxmove does |
| --- | --- |
| `2xx` | Done. |
| Timeout, network error, `408`, `409`, `425`, `429`, `5xx` | Retries with growing intervals, at most 15 minutes apart, up to 8 attempts in total. |
| Any other `4xx` | No retry. The event is kept as failed. |

After the last attempt, the event is kept as failed.

## Duplicates and order

Events are delivered at least once and not necessarily in order.

- **Deduplicate on `id`.** A retried event keeps its `id`, which is also sent in the `x-event-id` header.
- **Don't rely on arrival order.** Use `data.status` and `data.updated_at` to decide whether an event is newer than what you already stored.

```ts Node.js
type MaxmoveEvent = {
  id: string;
  data: { id: string; status: string; updated_at: string };
};

async function handleEvent(event: MaxmoveEvent) {
  if (await db.processedEvents.exists(event.id)) return; // duplicate
  const stored = await db.deliveries.find(event.data.id);
  if (!stored || new Date(event.data.updated_at) > new Date(stored.updated_at)) {
    await db.deliveries.upsert(event.data); // newer than what we have
  }
  await db.processedEvents.insert(event.id);
}
```

```python Python
from datetime import datetime

def handle_event(event: dict) -> None:
    if db.processed_events.exists(event["id"]):
        return  # duplicate
    stored = db.deliveries.find(event["data"]["id"])
    updated_at = datetime.fromisoformat(event["data"]["updated_at"].replace("Z", "+00:00"))
    if stored is None or updated_at > stored.updated_at:
        db.deliveries.upsert(event["data"])  # newer than what we have
    db.processed_events.insert(event["id"])
```

`db` stands for your own storage.

## Replay failed events

After you fix an outage on your side, send all failed events of an endpoint again with [`POST /v1/webhooks/{webhookId}/replay`](https://api.maxmove.com/v1/docs#tag/webhooks/POST/v1/webhooks/{webhookId}/replay). The response tells you how many events were queued:

```json
{ "queued": 3 }
```

Replayed events keep their `id`, so your deduplication still works, and they get the full number of attempts again. Failed events are not kept indefinitely, so replay them soon after you fix the problem.

## Disabled endpoints

A disabled endpoint receives no new events. Events that were already queued for it fail without further retries. Enable the endpoint again with `PATCH /v1/webhooks/{webhookId}` and `"status": "active"`, then replay its failed events.

## Missed events

Recover events after an outage with [`GET /v1/events`](https://api.maxmove.com/v1/docs#tag/events/GET/v1/events). The list includes events whether or not a webhook endpoint received them, scoped to your workspace and the API key's test or live mode. The key needs `deliveries:read`.

Filter by `delivery_id`, `type`, `created_after` (inclusive), or `created_before` (exclusive). Results arrive newest first, up to 100 per page. Pass `next_page_token` as `page_token` until it is `null`, keeping the same filters, and deduplicate by event `id`.

To inspect an event and its webhook delivery attempts, use [`GET /v1/events/{eventId}`](https://api.maxmove.com/v1/docs#tag/events/GET/v1/events/{eventId}). Each attempt includes its status, response code, and latency.

For the current delivery state, read [`GET /v1/deliveries/{deliveryId}`](https://api.maxmove.com/v1/docs#tag/deliveries/GET/v1/deliveries/{deliveryId}) or [`GET /v1/deliveries`](https://api.maxmove.com/v1/docs#tag/deliveries/GET/v1/deliveries).

---

Source: https://maxmove.com/en/developers/docs/webhooks/retries
