# Retries and delivery logs

How Pozy retries failed webhook deliveries (7 attempts over about 33 hours), when an endpoint is switched off, and how to read logs and redeliver events.

Endpoints go down: deploys, outages, an expired certificate. Pozy keeps trying, so a short outage on your side does not lose events. This page covers the retry schedule, what counts as a failure, what happens when an endpoint keeps failing, and the tools to inspect and replay deliveries.

## What counts as delivered

A delivery succeeds when your endpoint answers with any `2xx` status within **5 seconds**. Everything else is a failure and schedules another attempt:

| Outcome | Logged as |
| --- | --- |
| A status outside 200 to 299, including `3xx` redirects (they are not followed) | `Endpoint returned 503` |
| No response within 5 seconds | `No response within 5s` |
| DNS, TLS or connection error | `Connection failed: ...` |

The body of your response does not matter.

## The retry schedule

Each event is attempted up to **7 times** in total. The delay is counted from the previous attempt and grows each time, with the longest gap capped at 24 hours:

| Attempt | Delay after the previous attempt | Roughly this long after the event |
| --- | --- | --- |
| 1 | Immediately | 0 |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 30 minutes | 36 minutes |
| 5 | 2 hours | 2 hours 36 minutes |
| 6 | 6 hours | 8 hours 36 minutes |
| 7 | 24 hours | 32 hours 36 minutes |

Retries are processed every minute, so an attempt can arrive up to a minute after its slot. Every attempt carries the same body, the same `X-Pozy-Event-Id` and an `X-Pozy-Attempt` header with its number. The signature is computed again at each attempt with the endpoint's current secret.

If the 7th attempt fails, the delivery is marked `failed` and Pozy stops trying. The event is not lost: it stays in the logs and you can [redeliver](#redeliver-an-event) it once your endpoint is back.

> **Recover from an outage:**
> 
> After a long outage, redeliver the failed deliveries from the logs instead of waiting for the next scheduled attempt. Because every redelivery keeps the event id, your [deduplication](https://pozy.ai/docs/webhooks/idempotency) skips anything you already processed.

## When an endpoint is switched off

Each endpoint keeps a `failureCount`: the number of deliveries in a row that used all 7 attempts without success. A single successful delivery resets it to 0.

When `failureCount` reaches **20**, Pozy sets the endpoint's `isActive` to `false` and stops sending to it. This protects you from a backlog of events piling up against a URL that no longer exists. The workspace's owners and admins get a warning email at 10 failed deliveries in a row, and another email when Pozy turns the endpoint off at 20.

A turned-off endpoint gets no deliveries at all. Retries that were still pending for it close with `Not sent: this webhook is turned off` in the logs, and new events are not queued for it. The same applies to an endpoint you turn off yourself.

To turn it back on, fix the endpoint and update it:

```bash cURL
curl -X PUT https://api.pozy.ai/v1/webhooks/settings \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "webhookId": "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K", "isActive": true }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/webhooks/settings", {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ webhookId: "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K", isActive: true }),
});
if (!res.ok) throw new Error((await res.json()).error);
```
```python Python
requests.put(
    "https://api.pozy.ai/v1/webhooks/settings",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={"webhookId": "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K", "isActive": True},
    timeout=30,
).raise_for_status()
```

Re-enabling resets `failureCount` to 0. Events that happened while the endpoint was off were not queued for it, so once it is back on, redeliver the ones you need from the logs. Turn the endpoint on before you redeliver: a redelivery to a turned-off endpoint is not sent.

Watch `failureCount` and `lastFiredAt` from `GET /v1/webhooks/settings` in your monitoring to catch a failing endpoint before it is switched off. Test deliveries never count toward it.

## Read the delivery logs

`GET /v1/webhooks/logs` lists deliveries, newest first. There is one log entry per delivery; `attemptNumber` shows how many attempts it has had so far.

```bash cURL
curl "https://api.pozy.ai/v1/webhooks/logs?status=failed&webhookId=whk_4Tq9Lm2Vx8Rb6Nc1Pz7K&limit=20" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/webhooks/logs?status=failed&webhookId=whk_4Tq9Lm2Vx8Rb6Nc1Pz7K&limit=20", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
for (const log of data.logs) console.log(log.event, log.statusCode, log.errorMessage);
```
```python Python
resp = requests.get(
    "https://api.pozy.ai/v1/webhooks/logs",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={"status": "failed", "webhookId": "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K", "limit": 20},
    timeout=30,
)
for log in resp.json()["logs"]:
    print(log["event"], log["statusCode"], log["errorMessage"])
```

```json
{
  "logs": [
    {
      "_id": "dlv_Qm3Zt8Kp1Wx6Bn4Rc9Ly",
      "webhookId": "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K",
      "webhookName": "Production",
      "eventId": "8e0b6f8a-3c51-4b7e-9d0a-2f4c6e1b9a77",
      "event": "post.published",
      "test": false,
      "url": "https://app.example.com/webhooks/pozy",
      "status": "failed",
      "statusCode": 502,
      "responseBody": "<html><body>Bad Gateway</body></html>",
      "errorMessage": "Endpoint returned 502",
      "attemptNumber": 7,
      "responseTime": 211,
      "nextAttemptAt": null,
      "createdAt": "2026-11-03T08:30:04.000Z"
    }
  ],
  "pagination": { "total": 1, "limit": 20, "skip": 0, "pages": 1, "hasMore": false }
}
```

| Query parameter | What it filters |
| --- | --- |
| `status` | `success` or `failed`. |
| `webhookId` | One endpoint. |
| `event` | One event name, such as `message.received`. |
| `eventId` | Every delivery of one event, across endpoints. |
| `limit`, `skip` | Page size (up to 100, default 50) and offset. |

A delivery that is still being retried shows `status: "pending"` and the time of the next try in `nextAttemptAt`. `responseBody` keeps the first 2,000 characters of your endpoint's answer, `responseTime` is in milliseconds and `requestPayload` holds the exact body that was sent.

## Redeliver an event

`POST /v1/webhooks/logs/redeliver` sends an event again, right away, with its original body and event id:

```bash cURL
curl -X POST https://api.pozy.ai/v1/webhooks/logs/redeliver \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "webhookId": "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K", "eventId": "8e0b6f8a-3c51-4b7e-9d0a-2f4c6e1b9a77" }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/webhooks/logs/redeliver", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ webhookId: "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K", eventId: "8e0b6f8a-3c51-4b7e-9d0a-2f4c6e1b9a77" }),
});
if (!res.ok) throw new Error((await res.json()).error);
```
```python Python
requests.post(
    "https://api.pozy.ai/v1/webhooks/logs/redeliver",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={"webhookId": "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K", "eventId": "8e0b6f8a-3c51-4b7e-9d0a-2f4c6e1b9a77"},
    timeout=30,
).raise_for_status()
```

- A redelivery is a new delivery with a new `X-Pozy-Delivery-Id`. If it fails, it follows the normal retry schedule.
- The event id stays the same, so your handler recognises an event it already processed.
- You can redeliver to any endpoint in the workspace, not only the one that received it first. That is a simple way to backfill a new endpoint with recent events. The endpoint's event and profile filters are not applied to a redelivery.

To replay every failed delivery of an outage, page through `GET /v1/webhooks/logs?status=failed&webhookId=...` and redeliver each `eventId`, pacing the calls to stay inside your [rate limit](https://pozy.ai/docs/getting-started/rate-limits).

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/webhooks/logs`](https://pozy.ai/docs/api/getWebhookLogs) | List webhook delivery logs |
| [`POST /v1/webhooks/logs/redeliver`](https://pozy.ai/docs/api/redeliverWebhookEvent) | Redeliver a webhook event |
| [`PUT /v1/webhooks/settings`](https://pozy.ai/docs/api/updateWebhookSettings) | Update webhook |
| [`GET /v1/webhooks/settings`](https://pozy.ai/docs/api/getWebhookSettings) | List webhooks |
