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

# Reliable Sends in TypeScript

> What the TypeScript client retries, how idempotency keys work, and every error a send can throw.

A send either resolves to a `SendResult` or throws an error that tells you what happened. This page covers the retry rules, the idempotency guarantees, and the full error surface, including the one distinction that matters most when catching: transport failures and configuration failures are different error types.

## Idempotency

If you omit `idempotencyKey`, the client mints one UUID per send and reuses it across every internal retry, so a flaky connection cannot produce a duplicate. That minted key protects only the retries inside that one call.

To make repeats safe across process restarts and queue redeliveries, pass your own key naming the business fact:

```ts theme={"languages":{"custom":["/languages/repost.json"]}}
await repost.webhooks.user.created({
  customerId: "customer_123",
  data: user,
  idempotencyKey: "user_123:created",
});
```

The server remembers caller-supplied keys for 24 hours; a repeat with the same payload returns the original message, and a repeat with a different payload is rejected. The details are on the [HTTP API page](/docs/send/publishing#idempotency).

## What retries automatically

| Outcome                                         | Retried                |
| ----------------------------------------------- | ---------------------- |
| Network failure, fetch error                    | Yes                    |
| Per-attempt timeout                             | Yes                    |
| `5xx`, `429`, `409`                             | Yes                    |
| Non-JSON body on a `2xx`                        | Yes                    |
| Any other `4xx` (`400`, `401`, `403`, `413`, …) | No, thrown immediately |

Up to three retries follow the initial attempt (four attempts total by default). Backoff starts at `retryBaseDelayMs`, uses bounded jitter, and never exceeds `retryMaxDelayMs` or the remaining operation deadline. Valid delta-seconds and HTTP-date `Retry-After` values can extend that delay. Every attempt is bounded by both `attemptTimeoutMs` and the remaining `operationTimeoutMs` budget.

## Handling errors

HTTP rejections throw `RepostPublishError`, which carries the HTTP `status`, parsed bounded JSON `body`, and structured delivery metadata. Network, timeout, cancellation, deadline, and overload failures throw `RepostTransportError`:

```ts theme={"languages":{"custom":["/languages/repost.json"]}}
import { RepostPublishError, RepostTransportError } from "@repost/client";

try {
  await repost.webhooks.user.created({ customerId: "customer_123", data: user });
} catch (err) {
  if (err instanceof RepostPublishError) {
    console.error(err.errorCode, err.deliveryState, err.status, err.body);
  } else if (err instanceof RepostTransportError) {
    console.error(err.errorCode, err.deliveryState, err.failureReason);
  }
  throw err;
}
```

Stable error messages come from the exported error catalog:

| Failure             | Message                          |
| ------------------- | -------------------------------- |
| HTTP rejection      | `Request was rejected`           |
| Per-attempt timeout | `Transport attempt timed out`    |
| Network failure     | `Transport I/O failed`           |
| Payload over 1 MiB  | `Request exceeds the size limit` |

The payload cap is checked before the first attempt, so an oversized event never reaches the network.

<Warning>
  Configuration, validation, serialization, transport, publish, and descriptor-version failures have distinct exported error classes. They all extend `RepostError` and expose the same structured metadata fields.
</Warning>

Validation errors are coded and name the exact path:

* `REQUIRED` at `$.id` when a required field is absent
* `[NULL_NOT_ALLOWED]` when `null` is passed for a non-nullable field
* `TYPE_MISMATCH`, `OUT_OF_RANGE`, `NON_FINITE`, `INVALID_DATETIME`, `INVALID_ENUM`, `INVALID_JSON`, and `INVALID_UNICODE` for malformed values

These are bugs to fix, not requests to retry.

## What goes on the wire

Payloads serialize deterministically: fields in schema declaration order under their `@map` wire names, absent optional fields omitted (or their `@default` injected), explicit `null` only where the schema allows it, and timestamps in the ISO-8601 millisecond UTC form. Input properties that aren't in the schema are dropped. A value you provide is never overridden by a default. The envelope has exactly the keys `type`, `timestamp`, `data`, in that order.

## Continue

<Columns cols={3} className="gap-y-4">
  <Card title="Testing" icon="flask-conical" href="/docs/send/typescript/testing" cta="Test" arrow="true">
    Exercise these paths without a network.
  </Card>

  <Card title="Configuration" icon="sliders-horizontal" href="/docs/send/typescript/configuration" cta="Configure" arrow="true">
    The transport options behind the retry behavior.
  </Card>

  <Card title="Delivery & retries" icon="shield-check" href="/docs/send/delivery" cta="After the 202" arrow="true">
    What happens to the message once Repost accepts it.
  </Card>
</Columns>
