> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flowpayroll.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Replay and recovery

> See every delivery and every attempt, resend one delivery, or replay a whole window after your receiver was down.

Your receiver will be down at some point. Nothing is lost when it is: every event and every attempt is recorded, and you can send anything again.

## The delivery log

* `GET /deliveries?endpointId={endpointId}` lists an endpoint's deliveries, newest first. Add `status=Failed&status=Pending` to see only what has not arrived. An unknown status is a `400` with `invalid_delivery_status`, not ignored.
* `GET /deliveries?eventId={eventId}` lists every endpoint one event went to.
* `GET /deliveries/{deliveryId}` shows one delivery and every attempt at it: when it was sent, the headers we sent, how long your server took, the status it answered and the start of its response body.

Deliveries are kept for a year and their attempts for 90 days. Events are kept for 90 days, and `GET /events` lists them, each with the body exactly as it was sent. Filter it by type with `eventType`, repeated for each type or family (`eventType=payslip.*&eventType=payroll_run.completed`), up to 10 per request; more is a `400` with `too_many_event_types`.

Every list is paged. Each page is full except the last; follow `paginationToken` until it is absent.

### Catch up from the event list

After your receiver was down, you can read what you missed from `GET /events` instead of replaying it. Ask for up to 100 events a page with `pageSize`, and pass the `content.metadata.paginationToken` from each answer back as `paginationToken` for the next page. Every page is full except the last, whose `paginationToken` is `null`. Events come newest first. Each one's `envelope` is the body exactly as it was delivered and its `eventId` is the `webhook-id`, so the same [duplicate check](/api-reference/webhooks/best-practices#handle-duplicates) covers both. The token is short-lived: use it straight away and do not store it.

<CodeGroup>
  ```javascript Node.js theme={null}
  const base = "https://api.sandbox.flowpayroll.ai/v1/webhooks"
  const headers = { "X-Auth-Token": process.env.TOKEN, "X-Org-Id": process.env.ORGANISATION_ID }

  async function* listEvents(startDate) {
    let paginationToken = null
    do {
      const query = new URLSearchParams({ startDate, pageSize: "100" })
      if (paginationToken) query.set("paginationToken", paginationToken)
      const response = await fetch(`${base}/events?${query}`, { headers })
      if (!response.ok) throw new Error(`GET /events answered ${response.status}: ${await response.text()}`)
      const { content } = await response.json()
      yield* content.data
      paginationToken = content.metadata.paginationToken
    } while (paginationToken)
  }

  for await (const event of listEvents("2026-10-01")) {
    await processEvent(event.envelope)
  }
  ```

  ```python Python theme={null}
  # pip install requests
  import os
  import requests

  BASE = "https://api.sandbox.flowpayroll.ai/v1/webhooks"
  HEADERS = {"X-Auth-Token": os.environ["TOKEN"], "X-Org-Id": os.environ["ORGANISATION_ID"]}


  def list_events(start_date):
      params = {"startDate": start_date, "pageSize": 100}
      while True:
          response = requests.get(f"{BASE}/events", headers=HEADERS, params=params, timeout=30)
          response.raise_for_status()
          content = response.json()["content"]
          yield from content["data"]
          token = content["metadata"].get("paginationToken")
          if not token:
              return
          params["paginationToken"] = token


  for event in list_events("2026-10-01"):
      process_event(event["envelope"])
  ```
</CodeGroup>

### What Flow Payroll keeps

| Record | Kept for | What it holds |
| - | - | - |
| Event | 90 days | The event as sent, including a `Full` body with its personal data |
| Delivery | 1 year | Ids, status, number of attempts and timing. No event body |
| Attempt | 90 days | The headers we sent, with custom header values shown as `[redacted]`, and the status, headers and first 16 KB of the body your server answered with |
| Replay | 1 year | Its filters and progress |

When its time is up, a record is deleted automatically, usually within a few days. These records are encrypted at rest. Signing secrets, signing keys and custom header values are also encrypted with an encryption key used only by the Webhooks service.

## Resend one delivery

`POST /deliveries/{deliveryId}/replays` sends a finished delivery again, straight away, as a new delivery that points back at the original. It carries the same event id in `webhook-id`, so a receiver that [handles duplicates](/api-reference/webhooks/best-practices#handle-duplicates) is safe. Send an `Idempotency-Key` header to make sure a retried request resends only once.

A delivery that is still being retried cannot be resent until it finishes.

## Replay a window

After an outage, replay everything that failed:

```bash theme={null}
curl -X POST https://api.sandbox.flowpayroll.ai/v1/webhooks/replays \
  -H "X-Auth-Token: $TOKEN" \
  -H "X-Org-Id: $ORGANISATION_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2026-10-01",
    "endDate": "2026-10-03",
    "statuses": ["Failed"]
  }'
```

The answer is `202 Accepted` with the replay's id. It runs in the background: follow it with `GET /replays/{replayId}`, which shows how many deliveries it has resent so far and when it finished.

| Field | Meaning |
| - | - |
| `startDate`, `endDate` | The window, by when the events happened. Up to 90 days. |
| `statuses` | Which earlier outcomes to resend: `Failed` (the default), `Skipped` or `Succeeded`. |
| `eventTypes` | Only these types or families, such as `["payslip.*"]`. |
| `endpointIds` | Only these endpoints. Leave it out for all of them. |
| `includeNeverSent` | Also send events from the window that an endpoint never received at all, for example because it was subscribed to them later. |
| `maxDeliveries` | A ceiling on how many it sends. 10,000 by default, up to 100,000. |

A replay never sends the same thing twice, even if it is interrupted and runs again. You can run three at a time. Cancel one that is still running with `PATCH /replays/{replayId}` and `{"status": "Cancelled"}`.

## After your endpoint was disabled

1. Fix the cause. The endpoint's `statusReason` and its last deliveries' `lastErrorToken` say what went wrong.
2. `PATCH /endpoints/{endpointId}` with `{"status": "Active"}`.
3. Replay the window with `"statuses": ["Failed", "Skipped"]`: `Failed` for what was tried before it was disabled, `Skipped` for what happened after.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.