AUTH4D-307 Resending a failed webhook delivery
A webhook delivery (AUTH4D-157) that used up its six attempts, or was ended early with
endpoint_disabled, endpoint_deleted or invalid_url, used to stay failed for good. Owners
and admins can now put a failed delivery back in the queue from the console delivery log or the
management API. The delivery log keeps each row, with its payload and event id, for 30 days, so any
failed delivery still in the log can be resent.
Route
| Method and path | Scope / role | Result |
|---|---|---|
POST /management/tenants/{tenantId}/webhooks/{endpointId}/deliveries/{deliveryId}/redeliver |
tenant:write, owner or admin, {} |
200 { delivery }, no-store |
The body must be an empty JSON object (400 invalid_request otherwise), like the other webhook
action routes. The response is the existing WebhookDeliveryResponseSchema, so no contract change
was needed:
{
"delivery": {
"deliveryId": "whd_…",
"endpointId": "whe_…",
"eventId": "evt_…",
"eventType": "user.deleted",
"status": "pending",
"attempts": 0,
"lastResponseStatus": 503,
"lastError": null,
"nextAttemptAt": null,
"createdAt": "2026-10-21T08:00:00.000Z",
"updatedAt": "2026-10-22T09:15:00.000Z"
}
}
lastResponseStatus keeps the status of the last failed attempt until the next attempt replaces
it.
Rules
- One conditional
UPDATEfiltered bytenant_id,endpoint_id,delivery_idandstatus = 'failed'sets the row back topendingwithattempts = 0and clearsnext_attempt_at,lease_untilandlast_error. Only when it changes a row is oneWEBHOOK_DELIVERIESmessage sent. Two resends at once give one200and one409, and one message. - The resent request keeps the stored
webhook-id(the event id) and body, so receivers that deduplicate onwebhook-idhandle it once. It is signed when it is attempted, with the endpoint’s current secret, plus the previous one during a rotation overlap (AUTH4D-279). - The resend gets the normal six attempts and backoff.
Errors
| Status | Code | When |
|---|---|---|
400 |
invalid_request |
The body is not an empty JSON object. |
401 |
unauthorized |
No valid management token, or the token lacks tenant:write. |
403 |
forbidden |
The caller is not a member of the project. |
403 |
insufficient_role |
The caller is a viewer. The refusal is audited with outcome denied. |
404 |
not_found |
The endpoint, or the delivery under that endpoint, does not exist in the project. |
409 |
webhook_disabled |
The endpoint is turned off, so the resend would fail again at once. |
409 |
delivery_not_failed |
The delivery is pending, delivering or succeeded. |
409 |
project_suspended |
The project is suspended. |
503 |
temporarily_unavailable |
The WEBHOOK_DELIVERIES queue is not bound. The row stays failed. |
A delivery of another project, or of another endpoint in the same project, is 404, so the
caller cannot tell whether it exists.
Audit event
| Event | Target type | Metadata |
|---|---|---|
management.webhook.delivery_redelivered |
webhook_endpoint |
actor_role, delivery_id |
The audit row is written in the same D1 batch as the update, guarded by the same
status = 'failed' condition, so it is written exactly when a resend happens. Management events
are checked by AuditEventSchema (AUTH4D-228) and delivery_id was already used by
management.webhook.test_sent, so the observability SAFE_METADATA allowlist needs no new keys.
Console
The delivery log shows Resend under the status of failed rows, for owners and admins only. The button’s accessible name includes the event and time. After a resend the row shows Queued, a status message says the delivery was queued again, and focus moves to that message. A refusal shows fixed wording (for example “Enable the endpoint before resending a delivery.”) and never the server’s message. The table’s scroll area can now be focused, so it scrolls by keyboard at 320px. The console BFF forwards the new path.
Tests
tests/integration/auth4d-307.webhook-redeliver.test.ts: a delivery driven tofailedby six503answers is resent after a secret rotation, and the next attempt carries the samewebhook-idand body, verifies with the new secret only and leaves the rowsucceeded; a delivery dropped withendpoint_disabledis refused while the endpoint is off and resent once it is on;409for pending, delivering and succeeded rows; one200and one409for two resends at once;404across projects and endpoints; scope and role checks; the empty body; and503without the queue.tests/e2e/auth4d-307.webhook-redeliver.spec.ts(auth4d-307.playwright.config.ts, port 5461): Resend only on failed rows, a keyboard resend that turns the row to Queued, with axe checks at 320px and 1280px; refusals in plain wording; and a viewer without the action.
Source: docs/api-spec/auth4d-307.md