# Replay



Replay is the question &#x2A;"can I make this message happen again, exactly as it would have happened the first time?"* — answered with a single call instead of by hand-rolling SQL against an outbox table.

## The three replay verbs [#the-three-replay-verbs]

`postel.outbound.replay(opts)` takes one of three shapes, distinguished by which fields are present.

### By message id [#by-message-id]

```ts
await postel.outbound.replay({
  messageId:        "msg_2k4n8...",
  freshWebhookId:   false,         // keep the original id so receivers can dedup
});
```

Re-enqueues a single historical message to every endpoint that originally received it. The receiver sees the same `webhook-id` — if their dedup window is still open, they correctly skip it; if not, they process it again.

Pass `freshWebhookId: true` if you *want* the receiver to treat it as new (for testing receiver behavior, for instance).

### By endpoint + time range [#by-endpoint--time-range]

```ts
await postel.outbound.replay({
  endpointId:        "ep_payments_webhook",
  since:             "2026-05-23T00:00:00Z",
  until:             "2026-05-24T00:00:00Z",
  types:             ["order.created", "order.refunded"],
  replayThroughput:  50,     // messages/sec; throttles to avoid overwhelming the receiver
  freshWebhookId:    false,
});
```

The canonical operational verb: &#x2A;"customer X's webhook receiver was down from 02:00 to 04:00 last Tuesday; replay everything they should have received."*

### By predicate [#by-predicate]

```ts
await postel.outbound.replay({
  // filter receives the stored message as `unknown` — narrow it yourself
  filter: (msg) => {
    const m = msg as { type: string; data: { plan?: string } };
    return m.type === "user.signup" && m.data.plan === "enterprise";
  },
  replayThroughput: 20,
  freshWebhookId:   false,
});
```

The escape hatch for replays that can't be expressed as "endpoint + time range."

## What `replay` returns [#what-replay-returns]

```ts
type ReplayResult = { enqueued: number };
```

A count of outbox rows created. Each row goes through the normal delivery pipeline — same retries, same circuit breaker, same observability hooks.

## `reconcile` [#reconcile]

Distinct from replay: `reconcile({ endpointId, since })` **finds** messages that were never confirmed delivered to the given endpoint — the ids whose latest attempt is in a non-delivered state. Useful as a recurring background job: a healthy endpoint returns an empty page; an unhealthy one surfaces the backlog, which you can feed to `replay`.

```ts
const page = await postel.outbound.reconcile({
  endpointId: "ep_payments_webhook",
  since:      new Date(Date.now() - 86_400_000),
  limit:      500,                    // default 100
});
page.items;       // MessageId[] — oldest-first
page.nextCursor;  // resume token, or null once the backlog is exhausted
console.log(`${page.items.length} messages never confirmed delivered`);
```

The result is bounded — at most `limit` ids per call, oldest-first, with `nextCursor` resuming exactly where the previous page ended — so a reconcile over a long outage never materializes the entire backlog in one array. It is the same `{ items, nextCursor }` envelope every list read uses.

## Throughput control [#throughput-control]

The `replayThroughput` setting (messages/sec) is per-replay, not global. The worker schedules outbox rows tagged with a replay batch id; the rate limiter applies before the HTTP call. A receiver under load won't be flooded by a 100k-message replay.

## Read next [#read-next]

* [`send()` and the outbox](/docs/outbound/send) — what gets replayed.
* [Retries & backoff](/docs/outbound/retries) — how replayed attempts behave when they fail.
* [Endpoints](/docs/outbound/endpoints) — the lifecycle of the receiver-of-record.
