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

# Delivery and retries

> Understand response handling, retries, idempotency, and delivery status.

Wircle delivers events as HTTPS `POST` requests. Delivery is asynchronous and at least once: your endpoint may receive the same delivery more than once, so every handler must be idempotent.

## Successful delivery

Any HTTP status from `200` through `299` marks the delivery as succeeded. Wircle does not use the response body.

Verify the signature, durably record the delivery, and return a `2xx` response as soon as possible. Run slow work asynchronously after the event has been accepted.

Each request has a 15-second timeout. A timeout, connection error, or non-`2xx` response is treated as a failed attempt.

## HTTP response behavior

| Response                 | Result                                                   |
| ------------------------ | -------------------------------------------------------- |
| `200`–`299`              | Delivery succeeds and no more attempts are made.         |
| `410 Gone`               | Delivery fails permanently and the endpoint is disabled. |
| Any other status         | The attempt fails and is retried while attempts remain.  |
| Redirect (`3xx`)         | Treated as a failed attempt; redirects are not followed. |
| Timeout or network error | The attempt fails and is retried while attempts remain.  |

Return `410 Gone` only when you intentionally want Wircle to stop delivering to that endpoint. Once disabled, the endpoint no longer matches new events.

## Retry schedule

Wircle makes one initial attempt and up to nine retries. Delays are measured from the end of the preceding failed attempt.

| Attempt | Approximate delay before attempt |
| ------- | -------------------------------- |
| 1       | Immediately                      |
| 2       | 5 seconds                        |
| 3       | 5 minutes                        |
| 4       | 30 minutes                       |
| 5       | 2 hours                          |
| 6       | 5 hours                          |
| 7       | 10 hours                         |
| 8       | 14 hours                         |
| 9       | 20 hours                         |
| 10      | 24 hours                         |

After attempt 10 fails, the delivery is marked `failed`. Scheduling and worker availability can make an attempt occur slightly later than the listed delay.

## What remains stable across retries

| Value                   | Retry behavior                                            |
| ----------------------- | --------------------------------------------------------- |
| Event body              | Unchanged                                                 |
| Body `id`               | Unchanged event ID                                        |
| `webhook-id`            | Unchanged delivery ID                                     |
| Endpoint signing secret | Unchanged                                                 |
| Endpoint URL            | Uses the endpoint’s current URL, including an updated URL |
| `webhook-timestamp`     | Regenerated for every attempt                             |
| `webhook-signature`     | Regenerated for every attempt                             |

One event can match multiple endpoints. Those requests share the event body `id`, but each endpoint receives a different `webhook-id`. Deduplicate by `webhook-id` so one endpoint’s successful processing does not suppress another endpoint’s delivery.

## Idempotent receiver pattern

Use a transaction or another atomic operation to persist both the verified delivery ID and a queued work item before returning success:

```ts theme={null}
async function handleWebhook(request: Request) {
  const rawBody = await request.text();

  if (!verifySignature(request.headers, rawBody)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(rawBody);
  const deliveryId = request.headers.get('webhook-id')!;
  const accepted = await enqueueEventOnce({ deliveryId, event });

  if (!accepted) {
    return new Response(null, { status: 204 });
  }

  return new Response(null, { status: 202 });
}
```

`enqueueEventOnce` should use a transaction and enforce a unique constraint on the delivery ID. This prevents concurrent duplicates from both performing the same action without losing the event if the receiver stops between deduplication and queueing.

## Delivery statuses

| Status       | Meaning                                               |
| ------------ | ----------------------------------------------------- |
| `pending`    | Waiting for its first attempt or a scheduled retry.   |
| `delivering` | Currently claimed by a delivery worker.               |
| `succeeded`  | A delivery attempt received a `2xx` response.         |
| `failed`     | Retries were exhausted or delivery ended permanently. |

## Inspect delivery activity

Workspace owners and admins can view aggregate status and the 50 most recently updated deliveries in the Developer → Webhooks page or through:

```http theme={null}
GET /v1/workspaces/{workspace_id}/webhook-deliveries
```

Each delivery item includes:

| Field                   | Description                                                           |
| ----------------------- | --------------------------------------------------------------------- |
| `id`                    | Delivery ID, matching the `webhook-id` header                         |
| `webhook_event_id`      | Event ID, matching the body’s `id`                                    |
| `webhook_endpoint_id`   | Endpoint selected for the delivery                                    |
| `webhook_endpoint_name` | Current display name of that endpoint                                 |
| `event_type`            | Delivered event type                                                  |
| `profile_id`            | Workspace profile affected by the event                               |
| `status`                | Current delivery status                                               |
| `attempt_count`         | Number of completed attempts                                          |
| `last_status_code`      | Most recent HTTP status, when a response was received                 |
| `last_error`            | Most recent delivery error, when present                              |
| `next_attempt_at`       | Next scheduled attempt time; actionable while the delivery is pending |
| `delivered_at`          | Time a `2xx` response was received, when successful                   |
| `created_at`            | Time the delivery record was created                                  |
| `updated_at`            | Time the delivery record last changed                                 |

Use the activity history to distinguish receiver errors from connection failures. After correcting a URL, pending retries automatically use the updated endpoint URL.
