# Dead-letter & replay

> Find failed deliveries, inspect every HTTP attempt, and replay a terminal delivery as a new occurrence. Nothing is silently lost.

A delivery that runs out of retries isn't dropped — it's **dead-lettered**: kept, fully
inspectable, and replayable on demand. Every send attempt is recorded with its outcome,
status code, and timing, so you can see exactly why a delivery failed and re-run it once
the cause is fixed.

This is the **never silently lost** guarantee in practice. A failure is a row you can
query, not an event you missed.

## Find failed deliveries

List dead-lettered deliveries with the `status` filter:

```bash
curl -sS "https://api.schedstack.com/v1/deliveries?status=dead_letter" \
  -H "Authorization: Bearer sk_test_…"
```

```json
{
  "object": "list",
  "data": [
    {
      "id": "dlv_01J9ZK3F8Q",
      "object": "delivery",
      "schedule_id": "sch_01J9ZK2A7B",
      "mode": "test",
      "status": "dead_letter",
      "scheduled_for": "2026-06-27T09:00:00Z",
      "deadline": "2026-06-27T10:00:00Z",
      "next_fire_at": null,
      "attempt_count": 8,
      "last_status_code": 500,
      "idempotency_key": "occ_01J9ZK2A7B_1",
      "replay_of": null,
      "created_at": "2026-06-27T09:00:00Z",
      "finalized_at": "2026-06-27T09:42:11Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

Deliveries are returned newest-first. Combine filters to narrow the set:

- `status=` — any [delivery status](#delivery-status-reference): `dead_letter`, `expired`,
  `succeeded`, `scheduled`, and so on.
- `schedule_id=sch_…` — only deliveries from one schedule.
- `created_after=` / `created_before=` — RFC3339 timestamps to bound the window.
- `cursor=` / `limit=` — cursor pagination. Follow `next_cursor` while `has_more` is true
  (`limit` defaults to 20; keep it at 100 or below).

```bash
curl -sS "https://api.schedstack.com/v1/deliveries?status=dead_letter\
&schedule_id=sch_01J9ZK2A7B\
&created_after=2026-06-27T00:00:00Z" \
  -H "Authorization: Bearer sk_test_…"
```

A delivery reaches `dead_letter` when SchedStack gives up retrying. That happens in three
ways: retries ran out (`max_attempts` reached), the per-endpoint retry budget was exhausted
(default 10/sec, burst 100), or the very first attempt was a terminal
failure — a `4xx`/`3xx` response or a blocked target, which is never retried (so you can see
`dead_letter` with `attempt_count=1`). `expired` is a distinct terminal state — the delivery
passed its `ttl` deadline before it could land. Both are terminal and both are replayable.

## Inspect one delivery

Fetch a single delivery by id to see its final state and last response code:

```bash
curl -sS https://api.schedstack.com/v1/deliveries/dlv_01J9ZK3F8Q \
  -H "Authorization: Bearer sk_test_…"
```

`attempt_count` tells you how many times SchedStack tried; `last_status_code` is the code
from the final attempt. To see the full trail, list the attempts.

## Read the attempt trail

Each delivery records every HTTP attempt. List them oldest-first:

```bash
curl -sS https://api.schedstack.com/v1/deliveries/dlv_01J9ZK3F8Q/attempts \
  -H "Authorization: Bearer sk_test_…"
```

```json
{
  "object": "list",
  "data": [
    {
      "id": "att_01J9ZK3G10",
      "object": "attempt",
      "delivery_id": "dlv_01J9ZK3F8Q",
      "attempt_no": 1,
      "outcome": "retryable",
      "status_code": 503,
      "fired_at": "2026-06-27T09:00:00Z",
      "finished_at": "2026-06-27T09:00:00Z",
      "egress_ms": 214,
      "error": null
    },
    {
      "id": "att_01J9ZK9H44",
      "object": "attempt",
      "delivery_id": "dlv_01J9ZK3F8Q",
      "attempt_no": 8,
      "outcome": "terminal",
      "status_code": 500,
      "fired_at": "2026-06-27T09:42:10Z",
      "finished_at": "2026-06-27T09:42:11Z",
      "egress_ms": 1180,
      "error": "upstream returned 500"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

Each attempt's `outcome` tells you what SchedStack did next:

- `success` — your endpoint returned 2xx. The delivery is marked `succeeded`; no more
  attempts.
- `retryable` — a `408`, `429`, or `5xx` response, or a transport fault (timeout, connection
  refused, DNS/TLS error), which SchedStack retried per the schedule's
  [backoff policy](/docs/concepts/retries-and-dead-letter/).
- `terminal` — no further attempt. Either a non-retryable response — any other `4xx`, a
  `3xx` (redirects are disabled), or a blocked/invalid target — which fails immediately on
  the first attempt, or a retryable failure that ran out of retries or budget (or whose TTL
  deadline passed). The delivery moved to `dead_letter` / `expired`.

`status_code` is the HTTP status your endpoint returned (null if the request never
completed — a timeout or connection failure, with the cause in `error`). `egress_ms` is the
time SchedStack spent waiting on your endpoint for that attempt.

## Replay a delivery

Replaying creates a **new** delivery that re-sends the same occurrence, due immediately:

```bash
curl -sS -X POST \
  https://api.schedstack.com/v1/deliveries/dlv_01J9ZK3F8Q/replay \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: $(uuidgen)"
```

```js
await fetch(
  "https://api.schedstack.com/v1/deliveries/dlv_01J9ZK3F8Q/replay",
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SCHEDSTACK_API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
    },
  },
);
```

```python

httpx.post(
    "https://api.schedstack.com/v1/deliveries/dlv_01J9ZK3F8Q/replay",
    headers={
        "Authorization": f"Bearer {os.environ['SCHEDSTACK_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
)
```

You get back a fresh delivery (`201 Created`) whose `replay_of` points at the original. The
original is left untouched as the audit record:

```json
{
  "id": "dlv_01J9ZM0XYZ",
  "object": "delivery",
  "schedule_id": "sch_01J9ZK2A7B",
  "mode": "test",
  "status": "scheduled",
  "scheduled_for": "2026-06-27T11:05:00Z",
  "next_fire_at": "2026-06-27T11:05:00Z",
  "attempt_count": 0,
  "last_status_code": null,
  "idempotency_key": null,
  "replay_of": "dlv_01J9ZK3F8Q",
  "created_at": "2026-06-27T11:05:00Z",
  "finalized_at": null
}
```

A replay **re-resolves the target from the schedule** (and its endpoint profile, if any) at
replay time — it is a brand-new delivery, not a frozen snapshot of the old one. If you fixed
the bug by updating the endpoint or headers, the replay uses the current configuration.

A replay does **not** carry over the original's `idempotency_key` — the new delivery's
`idempotency_key` is `null`. Its outbound `Idempotency-Key` header is therefore the **new**
delivery id (`dlv_…`), which differs from the original's. A receiver that deduped the
original will **not** see the replay as a duplicate — it arrives as a new logical request.
If your handler must be safe to re-run, rely on [signature verification](/docs/guides/verify-signatures/)
plus your own at-least-once dedup, and treat a replay as an intentional re-delivery.

### Only terminal deliveries are replayable

Replay is allowed only when the original is in a **terminal** state — `dead_letter`,
`expired`, `succeeded`, or `canceled`. A delivery that is still in flight
(`scheduled`, `claimed`, `retry_scheduled`, `paused`) returns `409 Conflict`:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "not_replayable",
    "message": "Delivery is not in a replayable (terminal) state.",
    "request_id": "req_01J9ZM10AB"
  }
}
```

A delivery with no `schedule_id` cannot be replayed (there's no target to re-resolve) and
also returns `409 not_replayable`. An unknown id returns `404` with type `not_found_error`
and code `not_found` (`"message": "No such delivery."`).

## Delivery status reference

| Status | Terminal? | Replayable? | Meaning |
|---|---|---|---|
| `scheduled` | no | no | Waiting for its fire time. |
| `claimed` | no | no | A worker is sending it now. |
| `retry_scheduled` | no | no | A previous attempt failed; the next retry is queued. |
| `paused` | no | no | Held by a pause; not claimable. |
| `succeeded` | yes | yes | Your endpoint returned 2xx. |
| `dead_letter` | yes | yes | SchedStack gave up: retries exhausted, retry budget exhausted, or an immediate terminal failure (`4xx`/`3xx`/blocked target on attempt 1). |
| `expired` | yes | yes | Passed its `ttl` deadline before landing. |
| `canceled` | yes | yes | Canceled before it landed. |

## Related

- [Retries & backoff](/docs/concepts/retries-and-dead-letter/) — how `retry_policy` (`max_attempts`,
  backoff, `ttl`) decides when a delivery dead-letters.
- [MCP server](/docs/mcp/) — agents can list, inspect, and replay deliveries with the
  `replay_delivery` tool, no glue code required.
- [Idempotency](/docs/concepts/idempotency/) — how the outbound `Idempotency-Key` works, and
  why a replay (which carries no `idempotency_key`) is delivered as a new logical request.
