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

# Delivery and retries

> How Blujay retries failed deliveries, how to inspect attempts, and how to replay an event.

## At a glance

| | How it works |
| - | - |
| **Time to answer** | Your endpoint has 10 seconds to return a `2xx`. |
| **Retries** | Up to 10 attempts over about 25 minutes, on timeouts, network and DNS errors, `408`, `429` and `5xx`. |
| **Duplicates** | Delivery is at least once, so the same event can arrive more than once. Ignore repeats by `event_id`. |
| **Order** | Events can arrive out of order. Use `created_at` and the timestamps in `data` to decide what's newest. |
| **After that** | List every attempt and [replay](#replay-an-event) any event yourself with the API. |

## Success and retries

A delivery succeeds when your endpoint returns any `2xx` status within 10 seconds. Otherwise Blujay retries it automatically, up to **10 attempts in total over about 25 minutes**:

| Attempt | Wait before it |
| - | - |
| 1 | Sent right away |
| 2 | 10 seconds |
| 3 | 20 seconds |
| 4 | 40 seconds |
| 5 | 80 seconds |
| 6 | 160 seconds |
| 7–10 | 5 minutes each |

Each wait varies by up to 20% so retries don't arrive in bursts, and can be longer when Blujay is under heavy load.

### What your response means to Blujay

| Your endpoint returns | What Blujay does | Attempt `error_code` |
| - | - | - |
| `2xx` | Done. The body is ignored. | none |
| `301`, `302`, `303`, `307` or `308` with a `Location` header | Follows the redirect with the same `POST`, up to 3 times per attempt. See [Redirects](#redirects). | none, unless the redirect fails |
| Any other `3xx`, or a redirect without `Location`, or a `1xx` | Stops. Not retried. | `HTTP_UNEXPECTED_RESPONSE` |
| `408` | Retries on the schedule above. | `HTTP_RETRYABLE_RESPONSE` |
| `429` | Retries on the schedule above. `Retry-After` is not read. | `HTTP_RETRYABLE_RESPONSE` |
| Any other `4xx`, such as `400`, `401` or `404` | Stops. Fix your endpoint, then [replay](#replay-an-event) the event. | `HTTP_4XX_RESPONSE` |
| `5xx` | Retries on the schedule above. | `HTTP_RETRYABLE_RESPONSE` |
| Nothing within 10 seconds | Retries on the schedule above. | `WEBHOOK_DELIVERY_TIMEOUT` |
| A DNS failure | Retries on the schedule above. | `WEBHOOK_URL_UNRESOLVABLE` |
| A connection or TLS error | Retries on the schedule above. | `WEBHOOK_DELIVERY_NETWORK_ERROR` |

Every retry carries the same `event_id` and body, with a fresh signature. If your endpoint is down for longer than the retry window, replay the events you missed once it is back.

> [!TIP]
>
> To force a redelivery on purpose, for example when your database is unavailable, return `503`. The [Examples](/examples.md) page shows this pattern.

## Redirects

If your endpoint answers with a redirect (`301`, `302`, `303`, `307` or `308` and a `Location` header), Blujay follows it, up to 3 redirects per attempt:

* Blujay sends the same `POST`, with the full body and signature headers, to each new address. It never turns the request into an empty `GET`.
* Each step gets its own 10 seconds.
* Every address must be public HTTPS. A redirect to `http://` or to a private address fails the attempt with `WEBHOOK_URL_INVALID` or `WEBHOOK_URL_UNSAFE`. See [Where Blujay won't deliver](/webhooks/overview.md#where-blujay-wont-deliver).
* Too many redirects (`WEBHOOK_REDIRECT_LIMIT_EXCEEDED`) or a broken `Location` (`WEBHOOK_REDIRECT_INVALID`) isn't retried, because retrying wouldn't change the result. Fix the URL, then replay the event.

These codes are attempt `error_code` values from the deliveries list below, not API error codes; they have no error page.

Redirects usually mean the subscription URL is slightly off: `example.com` instead of `www.example.com`, or a missing or extra trailing `/`. To find the address your server really answers on, run this. It prints the final URL after any redirects:

```bash theme={null}
curl -s -o /dev/null -L -w '%{url_effective}\n' -X POST \
  -H 'content-type: application/json' -d '{}' \
  https://yourapp.example.com/webhooks/blujay
```

Then update the subscription's `url` with `PATCH /v1/webhook-subscriptions/{subscription_id}` so deliveries skip the redirect.

## Inspect delivery attempts

```bash theme={null}
curl "https://api.blujay.io/v1/webhook-subscriptions/whsub_abc123/deliveries?limit=25" \
  -H "Authorization: Bearer blu_your_api_key"
```

```json theme={null}
{
  "status": "success",
  "data": {
    "data": [
      {
        "event_id": "evt_abc123",
        "event_type": "message.received",
        "attempt_number": 1,
        "delivery_mode": "automatic",
        "status": "failed",
        "http_status": 503,
        "error_code": "HTTP_RETRYABLE_RESPONSE",
        "error_message": "Remote endpoint returned retryable status 503",
        "scheduled_at": "2026-10-14T17:05:00.000Z",
        "started_at": "2026-10-14T17:05:00.010Z",
        "completed_at": "2026-10-14T17:05:00.420Z",
        "next_retry_at": "2026-10-14T17:05:10.000Z",
        "created_at": "2026-10-14T17:05:00.000Z",
        "updated_at": "2026-10-14T17:05:00.420Z"
      }
    ],
    "has_more": false,
    "limit": 25
  },
  "trace_id": "trc_2c7a9e1b40d5",
  "timestamp": "2026-10-14T17:05:01.000Z"
}
```

Each row is one **attempt**, newest first. A failed attempt with `next_retry_at` will be retried; it isn't final. Add `event_id=evt_abc123` to see every attempt for one event; `cursor` and `limit` page the list. You can see the same log in [Blujay](https://app.blujay.io) under **API** → **Webhooks**.

### Find the event for a message

There is no endpoint that lists events, and no lookup from a `message_id` to its events. The `event_id` arrives in the `webhook-id` header and the `event_id` field of each delivery, so store it with the event. If you never received the event, page the deliveries list for the time window, which shows each `event_id` and `event_type` but not the message, and replay the events in that window; each replay carries the full body.

| `error_code` | Meaning |
| - | - |
| `HTTP_RETRYABLE_RESPONSE` | Your endpoint returned `408`, `429` or `5xx`. |
| `HTTP_4XX_RESPONSE` | Your endpoint returned another `4xx`. Not retried. |
| `HTTP_UNEXPECTED_RESPONSE` | Your endpoint returned a `1xx` or `3xx` that wasn't a followable redirect. Not retried. |
| `WEBHOOK_DELIVERY_TIMEOUT` | No response within 10 seconds. |
| `WEBHOOK_DELIVERY_NETWORK_ERROR` | The connection failed, including a TLS certificate Blujay couldn't verify. |
| `WEBHOOK_REDIRECT_LIMIT_EXCEEDED`, `WEBHOOK_REDIRECT_INVALID` | Too many redirects, or a redirect to a URL Blujay won't call. Not retried. |
| `WEBHOOK_URL_INVALID`, `WEBHOOK_URL_UNSAFE`, `WEBHOOK_URL_UNRESOLVABLE` | The URL isn't a valid public HTTPS address, resolves to a private address, or doesn't resolve. Only the last one is retried. |
| `WEBHOOK_ATTEMPT_LIMIT_EXHAUSTED` | All 10 attempts failed. Replay the event when your endpoint is fixed. |
| `WEBHOOK_DELIVERY_INTERRUPTED` | Blujay stopped this attempt before it finished. The event is retried, or you can replay it. |
| `WEBHOOK_SIGNING_SECRET_UNAVAILABLE` | Blujay couldn't sign the request, so it wasn't sent. It is retried. |
| `WEBHOOK_PAYLOAD_INVALID` | The stored event couldn't be prepared for delivery. Contact [support](/support.md) with the `event_id`. |
| `WEBHOOK_SUBSCRIPTION_INACTIVE`, `WEBHOOK_PHONE_NUMBER_EXCLUDED` | The subscription was disabled, or no longer includes the event's type or phone number, when Blujay tried to deliver. |
| `WEBHOOK_EVENT_NO_LONGER_DELIVERABLE` | The event can no longer be delivered, for example because the number it concerns was replaced. |

## Replay an event

After you fix your endpoint, send an event again:

```bash theme={null}
curl -X POST https://api.blujay.io/v1/webhook-subscriptions/whsub_abc123/events/evt_abc123/replay \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Idempotency-Key: replay-evt_abc123-1"
```

Blujay answers `202 Accepted` and makes one more delivery attempt, recorded with `delivery_mode: "replay"`. Check the deliveries list for its result.

* The replay has the original `event_id` and body, in the version the event was created with.
* If automatic retries for the event are still running, the replay waits for them to finish first.
* A replay that fails isn't retried automatically. Replay it again when you're ready.
* The subscription must be active and still include the event's type and phone number, or you get `409` [`WEBHOOK_SUBSCRIPTION_INACTIVE`](/errors/webhook-subscription-inactive) or [`WEBHOOK_PHONE_NUMBER_EXCLUDED`](/errors/webhook-phone-number-excluded).
* Reuse the same `Idempotency-Key` only to retry the same replay request. A different key sends the event again.
* Events stay replayable for at least the full retry window. Blujay doesn't publish a longer retention guarantee, so replay what you missed as soon as your endpoint is back.
* Only events Blujay recorded can be replayed. While a subscription is disabled, no events are recorded for it. See [Webhooks](/webhooks/overview.md#manage-subscriptions).

## Limitations

* The retry schedule is fixed. You can't change the number of attempts or the waits, and `Retry-After` on a `429` is ignored.
* Replay is one event at a time, to the subscription that recorded it. There is no bulk replay and no replay to a different subscription.
* Events from a disabled subscription are never recorded, so they can't be replayed. Pausing a subscription loses events.
* There is no events endpoint and no message-to-event lookup. Store `event_id` with every event you receive.
* Attempt `error_code` values are not API error codes. Only the API errors above link to an error page.

## Common mistakes

> [!WARNING]
>
> * Don't return a redirect. Point the subscription at the final URL; a redirect costs a round trip on every attempt and fails if it leaves public HTTPS.
> * Don't replay before you fix the endpoint. A replay that fails is not retried.
> * Don't pause a subscription to buffer events. Return `503` instead, so Blujay retries and keeps the events.

## Next steps

> **[Verify signatures](/webhooks/verify-signatures.md)**
>
> Every attempt is signed again.

> **[Events](/webhooks/events.md)**
>
> Every event type and its payload.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.