# Retries & delivery logs

How BeeL. retries failed deliveries and how to inspect the delivery history.

BeeL. automatically retries webhook deliveries that fail due to network errors, a `5xx` response, or a `408` / `429`. You can also inspect the delivery history and trigger manual retries from the dashboard or the API.

## Retry policy

| Parameter | Value |
|-----------|-------|
| Max attempts | **7** (1 original + the rest automatic retries) |
| Retry strategy | **Fixed schedule**: few attempts, spread out, see [Retry schedule](#retry-schedule) |
| Timeout per attempt | **10 seconds** |
| Whole automatic retry window | **67 hours 11 minutes** from the first attempt to the last (about 3 days) |
| Sandbox | Only the first **3** attempts, over in **11 minutes** |

Every attempt, automatic or manual, is signed afresh with the current time — see [Every attempt is signed afresh](/webhooks/signatures#every-attempt-is-signed-afresh).

### Retry schedule

| Attempt | Wait after the previous attempt | Since the first attempt | Sandbox |
|---|---|---|---|
| 1st (original) | Immediately | — | Yes |
| 2nd | 1 minute | 1 minute | Yes |
| 3rd | 10 minutes | 11 minutes | Yes |
| 4th | 1 hour | 1 hour 11 minutes | No |
| 5th | 6 hours | 7 hours 11 minutes | No |
| 6th | 24 hours | 31 hours 11 minutes | No |
| 7th | 36 hours | 67 hours 11 minutes | No |

If all 7 attempts fail, no further automatic retries occur. You can still trigger a **manual retry** from the dashboard or API at any time.

## How long the retries last

In production the automatic retries span **about 3 days**: the last attempt goes out 67 hours 11 minutes after the first. That rides out a dropped connection, a bad deploy or an outage of a day or two. In sandbox they stop after 3 attempts, 11 minutes after the first, so testing never leaves events waiting for days.

If your endpoint is down for longer than that, every automatic attempt of the events in that window fails, and BeeL. does not try them again on its own.

Plan for it:

1. **Know you missed something.** Watch `consecutive_failures` and `last_error_cause` on the subscription (see [Reading the health fields](#reading-the-health-fields)), or the email BeeL. sends when the endpoint keeps failing.
2. **Replay what failed.** List the deliveries of the subscription with [List webhook delivery logs](/webhooks/listAccountWebhookDeliveries), pick the attempts with `success: false` from the outage, and retry each with a [manual retry](#manual-retry). There is no age limit on a manual retry: any delivery in the log can be retried while its subscription exists.
3. **Or reconcile from the resources.** A webhook is a notification, not the record. Re-reading the invoices that changed during the outage — for example the list filters of [Reconcile, do not only listen](/verifactu/handling-rejections#reconcile-do-not-only-listen) — gives you the same state without replaying anything. Pace that job to the [rate limits](/guides/rate-limits).

Deduplicate on `BeeL-Event-Id` either way: a replayed event you already processed must be a no-op — see [Deduplication](/webhooks/deduplication).

## What triggers a retry

| Scenario | Retried? |
|----------|----------|
| Your server returns `5xx` | ✅ Yes |
| Your server returns `408 Request Timeout` or `429 Too Many Requests` | ✅ Yes, on the same schedule |
| Connection refused / DNS failure / TLS error / timeout | ✅ Yes |
| Your server returns any other `4xx` | ❌ No — treated as a permanent rejection of that delivery |
| Your server returns `2xx` | ❌ No — delivery successful |

<Callout type="info">
  If your endpoint returns a `4xx` other than `408` or `429`, BeeL. assumes the payload is the problem and will not retry. Return `5xx` if you need BeeL. to retry (e.g. your database is temporarily unavailable), and `429` if you want BeeL. to slow down rather than drop the event.
</Callout>

## Endpoint health and automatic pause

Besides retrying each delivery, BeeL. tracks whether your endpoint is receiving anything at all. A delivery counts as **failed** once it is given up on — it used all 7 attempts, or your endpoint rejected it with a non-retryable `4xx`. Any successful delivery resets the count.

| Consecutive failed deliveries | What happens |
|-------------------------------|--------------|
| **5** | The subscription's owner receives an email saying the endpoint is failing, with the last error. One email per run of failures. |
| **25**, over a run lasting **more than 48 hours** | BeeL. pauses the subscription and emails the owner again. Both conditions must hold: a busy endpoint that fails 25 times in an afternoon while you fix it is not paused. |

A paused subscription stops receiving events. Events that happen while it is paused are not queued for later, and deliveries still waiting for an automatic retry when it is paused are dropped, even if you reactivate it before their turn. The same happens when you turn a subscription off or delete it. Those deliveries stay in the log, so you can [retry them manually](#manual-retry).

### Reading the health fields

Every [webhook subscription](/webhooks/getAccountWebhookSubscription) carries its health, so you can monitor it without waiting for the email:

| Field | Meaning |
|-------|---------|
| `consecutive_failures` | Failed deliveries in a row. A delivery that exhausted its 7 attempts counts as one. `0` on a healthy endpoint. |
| `last_error` | The last error seen in the current run of failures, as text for a human. |
| `last_error_cause` | The same error as a stable code (`dns`, `connection`, `tls`, `timeout`, `http_client_error`, `http_server_error`, `invalid_url`, `forbidden_target`, `unknown`). Branch on this, never on the wording of `last_error`. |
| `active` | `false` once the subscription is paused or turned off. |
| `deactivated_by` | `beel` when BeeL. paused it for failing; `owner` when you turned it off. Empty while it is active. |
| `deactivated_at` | When it was turned off. `null` while it is active. |

`last_error` and `last_error_cause` are cleared, together with `consecutive_failures`, by the first successful delivery.

### Reactivating a paused subscription

Send `"active": true` with [Update a webhook subscription](/webhooks/patchAccountWebhookSubscription). If BeeL. paused it (`deactivated_by: beel`), the same call first sends a signed test delivery to your URL:

- The endpoint answers `2xx` → the subscription is active again and its failure count starts from zero.
- Anything else → the call is rejected with `422` [`WEBHOOK_REACTIVATION_REQUIRES_LIVE_ENDPOINT`](/errors/WEBHOOK_REACTIVATION_REQUIRES_LIVE_ENDPOINT) and the subscription stays paused. Fix the endpoint and try again.

A subscription you turned off yourself (`deactivated_by: owner`) reactivates without a test.

## Delivery logs

BeeL. keeps a log of **every delivery attempt** per subscription, newest first. Each log entry records:

- Delivery timestamp and duration
- HTTP status code and response body
- All request headers sent (including `BeeL-Signature`, `BeeL-Event-Id`, `BeeL-Delivery-Id`, `BeeL-Event`)
- The full JSON payload that was sent
- The attempt number
- Error description (for failed deliveries)

### View delivery logs

`GET /v1/accounts/{account_id}/webhooks/{webhook_id}/deliveries` — **`webhooks:read`**. Newest first,
paginated. Query params: `page`, `limit`, `event_type` (only deliveries of one event
type) and `event_id` (only deliveries of one event — use it to follow every attempt on
a single event without paging through the whole history).

```bash
curl "https://app.beel.es/api/v1/accounts/{account_id}/webhooks/3f7a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c/deliveries?event_type=verifactu.status.updated" \
  -H "Authorization: Bearer $BEEL_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "deliveries": [
      {
        "id": "8ee6b023-c4e5-482e-93ca-dc66da2f9cb5",
        "subscription_id": "3f7a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
        "webhook_event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "event_type": "verifactu.status.updated",
        "attempt_number": 1,
        "http_status": 200,
        "success": true,
        "duration_ms": 142,
        "response_body": "OK",
        "response_body_truncated": false,
        "response_body_length": 2,
        "request_headers": {
          "BeeL-Signature": "t=1741362026,v1=3c4f7a2e...",
          "BeeL-Event": "verifactu.status.updated",
          "BeeL-Event-Id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
          "BeeL-Delivery-Id": "8ee6b023-c4e5-482e-93ca-dc66da2f9cb5",
          "Idempotency-Key": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        },
        "payload": "{\"id\":\"a1b2c3d4...\",\"type\":\"verifactu.status.updated\",...}",
        "delivered_at": "2026-03-07T16:20:26Z"
      }
    ],
    "pagination": {
      "current_page": 1,
      "total_pages": 1,
      "total_items": 1,
      "items_per_page": 20,
      "has_next": false,
      "has_previous": false
    }
  }
}
```

### Reading a log entry

Every field of the entry is described in
[List webhook delivery logs](/webhooks/listAccountWebhookDeliveries). What the schema cannot
tell you is which of them answer which question:

- **"Is this the same event I already saw?"** — `webhook_event_id` groups every attempt
  at one logical event and matches the `BeeL-Event-Id` header; `id` identifies *this*
  attempt and matches `BeeL-Delivery-Id`. Deduplicate on the former, correlate your
  server logs with the latter.
- **"Did my endpoint refuse it, or never see it?"** — `http_status` is `null` when the
  connection itself failed (DNS, timeout, TLS), and `error_message` says which. On a
  successful attempt `error_message` is absent.
  A populated `http_status` means your server answered and the payload reached it.
- **"Can I replay it myself?"** — `request_headers` and `payload` hold the exact bytes
  and signature that were sent, so you can re-run your verification offline against a
  delivery that failed, without waiting for the next one. Skip the timestamp check when
  you do: the logged `t` is as old as the attempt, so it is outside the replay window.

## Manual retry

`POST /v1/accounts/{account_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/retry` —
**`webhooks:write`**. Triggers a retry for any delivery, successful or not:

```bash
curl -X POST \
  "https://app.beel.es/api/v1/accounts/{account_id}/webhooks/3f7a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c/deliveries/8ee6b023-c4e5-482e-93ca-dc66da2f9cb5/retry" \
  -H "Authorization: Bearer $BEEL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

This uses the **original payload** and generates a new signature with a fresh timestamp. It runs right away and answers with the new delivery log entry — the next `attempt_number`, the same `webhook_event_id` and `BeeL-Event-Id`, a new `BeeL-Delivery-Id`. Branch on its `success`, not on the HTTP status. A manual retry that fails does not add to `consecutive_failures`.

## Endpoint requirements

To ensure reliable delivery, your endpoint should:

- **Respond within 10 seconds** — BeeL. does not wait longer
- **Return `2xx` immediately** and process the event asynchronously if needed
- **Return `5xx`** if you are temporarily unable to process (triggers a retry)
- **Return `429`** if you need BeeL. to slow down (triggers a retry)
- **Not return any other `4xx`** unless the payload itself is the problem

```js
// ✅ Good: acknowledge immediately, process async
app.post('/webhooks/beel', async (req, res) => {
  res.status(200).send('OK'); // respond first
  await queue.push(req.body); // process later
});

// ❌ Bad: synchronous processing blocks the response
app.post('/webhooks/beel', async (req, res) => {
  await processEventSynchronously(req.body); // may exceed 10s timeout
  res.status(200).send('OK');
});
```

## Related

<Related>

- [Deduplication](/webhooks/deduplication) — handle the repeats retries produce
- [Signature verification](/webhooks/signatures) — every attempt is signed afresh
- [Webhook events](/webhooks/events) — the payloads being retried

</Related>

---

Full OpenAPI spec: https://docs.beel.es/api/openapi