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

# Deliveries

> Retry schedule, delivery inspection, and manual replays.

Each event triggers one delivery row per subscribed endpoint. The delivery row tracks attempts, the most recent HTTP status, and the next scheduled retry. You can inspect deliveries from the dashboard or via the API.

## Statuses

| Status       | Meaning                                                            |
| ------------ | ------------------------------------------------------------------ |
| `pending`    | Queued. The next scheduled attempt is in `next_attempt_at`.        |
| `delivering` | An attempt is in progress.                                         |
| `succeeded`  | An attempt returned `2xx`. Terminal.                               |
| `failed`     | All attempts exhausted without a `2xx`. Terminal until you replay. |

## Retry schedule

A failed delivery is retried up to seven times after the initial attempt, for eight total attempts. The Workflow-backed retry schedule is:

| Attempt | Delay before this attempt |
| ------- | ------------------------- |
| 1       | 0 (immediate)             |
| 2       | 5 seconds                 |
| 3       | 5 minutes                 |
| 4       | 30 minutes                |
| 5       | 2 hours                   |
| 6       | 5 hours                   |
| 7       | 10 hours                  |
| 8       | 10 hours                  |

A delivery that finishes attempt 8 with a non-`2xx` response is marked `failed` and stops retrying. The automatic retry window is about 32 hours and 35 minutes after the first attempt.

A delivery is treated as failed if:

* The HTTP request fails to connect or times out.
* The endpoint returns a non-`2xx` status.
* The endpoint returns no response within the request timeout.

Slow endpoints are the most common cause of unintended retries. Acknowledge with `2xx` first and process asynchronously if your handler is heavy.

## Inspecting deliveries

```bash theme={null}
curl "https://api.kayle.id/v1/webhooks/deliveries?status=failed&limit=50" \
  -H "Authorization: Bearer kk_..."
```

Filter by `status`, `endpoint_id`, or `event_id`. The response is the standard cursor-paginated list:

```json theme={null}
{
  "data": [
    {
      "id": "whd_...",
      "event_id": "evt_...",
      "webhook_endpoint_id": "whe_...",
      "webhook_encryption_key_id": null,
      "status": "failed",
      "attempt_count": 3,
      "next_attempt_at": null,
      "payload_expires_at": "2026-05-08T11:03:30Z",
      "payload_scrubbed_at": null,
      "payload_retention_reason": "terminal_failure_retention",
      "last_status_code": 502,
      "last_attempt_at": "2026-05-05T11:03:30Z",
      "created_at": "2026-05-05T11:00:00Z",
      "updated_at": "2026-05-05T11:03:30Z"
    }
  ],
  "pagination": { "limit": 50, "has_more": false, "next_cursor": null },
  "error": null
}
```

## Payload retention

Webhook payloads are JWE-encrypted, but they can still contain personal data once your endpoint decrypts them. Kayle keeps the encrypted body only while it is needed for automatic delivery and manual recovery.

* `succeeded` deliveries have `payload_scrubbed_at` set and the encrypted payload removed immediately after the first `2xx` response.
* `failed` deliveries keep the encrypted payload for the endpoint's undelivered payload retention window after the final automatic attempt.
* Expired, missing-key, and JWE-creation failures cannot be retried because there is no retained encrypted payload to send.

The endpoint setting `undelivered_payload_retention_hours` controls the manual retry/replay window after final delivery failure. Supported values are `0`, `24`, `72`, and `168`; new endpoints default to `72`.

## Manual retry

Replay a single failed delivery:

```bash theme={null}
curl -X POST https://api.kayle.id/v1/webhooks/deliveries/whd_.../retry \
  -H "Authorization: Bearer kk_..."
```

The delivery moves back to `pending` and runs through the automatic retry schedule again. The signature is regenerated, so don't expect the captured headers from the previous attempts to match.

Manual retry is only available while the delivery still has a retained encrypted payload. Once `payload_expires_at` passes, or once `payload_scrubbed_at` is set, the API returns `409 WEBHOOK_PAYLOAD_EXPIRED`.

## Replaying an event

If multiple deliveries (one per subscribed endpoint) failed because of a downstream outage, replay the whole event rather than each delivery:

```bash theme={null}
curl -X POST https://api.kayle.id/v1/webhooks/events/evt_.../replay \
  -H "Authorization: Bearer kk_..."
```

Replay requeues the existing deliveries for that event while their encrypted payloads are still retained. It does not create deliveries for endpoints that were added after the event fired. If every delivery for the event has been scrubbed or expired, the API returns `409 WEBHOOK_PAYLOAD_EXPIRED`.

## Listing events

```bash theme={null}
curl "https://api.kayle.id/v1/webhooks/events?type=verification.session.succeeded" \
  -H "Authorization: Bearer kk_..."
```

`/v1/events` is an alias for the same resource, kept for older integrations. Use the `/v1/webhooks/events` path in new code.
