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 |
| 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.
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:
- Know you missed something. Watch
consecutive_failuresandlast_error_causeon the subscription (see Reading the health fields), or the email BeeL. sends when the endpoint keeps failing. - Replay what failed. List the deliveries of the subscription with List webhook delivery logs, pick the attempts with
success: falsefrom the outage, and retry each with a manual retry. There is no age limit on a manual retry: any delivery in the log can be retried while its subscription exists. - 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 — gives you the same state without replaying anything. Pace that job to the rate limits.
Deduplicate on BeeL-Event-Id either way: a replayed event you already processed must be a no-op — see 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 |
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.
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.
Reading the health fields
Every webhook subscription 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. 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
422WEBHOOK_REACTIVATION_REQUIRES_LIVE_ENDPOINTand 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).
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"{
"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. What the schema cannot tell you is which of them answer which question:
- "Is this the same event I already saw?" —
webhook_event_idgroups every attempt at one logical event and matches theBeeL-Event-Idheader;ididentifies this attempt and matchesBeeL-Delivery-Id. Deduplicate on the former, correlate your server logs with the latter. - "Did my endpoint refuse it, or never see it?" —
http_statusisnullwhen the connection itself failed (DNS, timeout, TLS), anderror_messagesays which. On a successful attempterror_messageis absent. A populatedhttp_statusmeans your server answered and the payload reached it. - "Can I replay it myself?" —
request_headersandpayloadhold 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 loggedtis 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:
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
2xximmediately and process the event asynchronously if needed - Return
5xxif you are temporarily unable to process (triggers a retry) - Return
429if you need BeeL. to slow down (triggers a retry) - Not return any other
4xxunless the payload itself is the problem
// ✅ 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');
});