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:
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.
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:
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.
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.
Validation errors are coded and name the exact path:
REQUIREDat$.idwhen a required field is absent[NULL_NOT_ALLOWED]whennullis passed for a non-nullable fieldTYPE_MISMATCH,OUT_OF_RANGE,NON_FINITE,INVALID_DATETIME,INVALID_ENUM,INVALID_JSON, andINVALID_UNICODEfor 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.