A send either returns a SendResult or throws a RepostException subclass that tells you exactly what happened and whether the event may already have reached Repost. This page covers the outcome model end to end.
Idempotency#
Repost dedupes on the idempotency key. When you don't pass one, the runtime mints a stable key per operation 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.Book.CreatedAsync(new BookCreatedInput
{
CustomerId = "cus_123",
Data = book,
IdempotencyKey = "book_dune: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.
Delivery outcomes#
Every failure is a RepostException carrying a stable ErrorCode, a best-known DeliveryState, the IdempotencyKey, the started AttemptCount, and a Retryable flag, never a raw server body, header, or nested exception. The five delivery states:
DeliveryState | Meaning | What to do |
|---|---|---|
Accepted | Repost accepted the event. | Done: you get a SendResult, not an exception. |
NotSent | The request definitely never left. | Safe to retry with the same key. |
PossiblySent | The request may have been received; the response was lost, ambiguous (HTTP 409), or the deadline elapsed mid-flight. | Reuse the same idempotency key and reconcile, never mint a new key, or you risk a duplicate. |
Rejected | Repost definitively rejected it (a non-retryable 4xx). | Fix the request; do not retry blindly. |
CancelledUnknown | You cancelled after the request may have been sent. | Same as PossiblySent: reconcile with the same key. |
try
{
SendResult result = await repost.Webhooks.Book.CreatedAsync(input);
Console.WriteLine($"accepted {result.Id}");
}
catch (RepostException failure)
{
if (failure.DeliveryState == DeliveryState.PossiblySent)
{
// Repost may already have the event. Reuse the SAME idempotency key
// and reconcile — never mint a new key, or you risk a duplicate.
Reconcile(failure.IdempotencyKey);
}
else if (failure.Retryable)
{
// Definitely not sent and safe to retry with the same idempotency key.
RetryLater(input, failure.IdempotencyKey);
}
else
{
// Terminal failure. Inspect the low-cardinality code; the message and
// exception carry no payload or credentials.
throw new InvalidOperationException($"send failed: {failure.ErrorCode}");
}
}The exception subclass tells you the category without inspecting a code: RepostConfigurationException (bad config), RepostValidationException (your model failed validation, NotSent), RepostSerializationException, RepostTransportException (connect/TLS/timeout/cancel/overload), RepostPublishException (the server rejected the request or returned a retryable failure), and RepostDescriptorVersionException (regenerate or upgrade: the generated client and runtime are incompatible).
What retries automatically#
Up to three retries follow the initial attempt (four attempts total by default). Retryable outcomes include DNS, connect, proxy, TLS, and I/O failures, an attempt timeout, 5xx, 429, 409, and a malformed 2xx body. Any other 4xx (400, 401, 403, 413, …) is thrown immediately. Backoff is full jitter in integer milliseconds from zero through min(60_000, RetryBaseDelay × 2^(attempt−1)); a valid Retry-After delta or HTTP-date raises that delay, still capped at 60 seconds. Each attempt gets a fresh timeout budget; the whole operation is bounded by OperationTimeout.
A payload over 1 MiB is rejected before the first attempt with a RepostSerializationException (RequestTooLarge), so an oversized event never reaches the network.
Cancellation#
Every send method takes an optional CancellationToken. Cancelling before the request commits settles NotSent; cancelling after it may have been written settles CancelledUnknown: reconcile with the same key. Either way the operation settles exactly once and closes only the work it owns.
Bounded admission#
MaxInFlightOperations is a hard bulkhead, not a queue. When more operations are in flight than the budget allows, a new send is rejected immediately with a RepostTransportException whose ErrorCode is Overloaded and DeliveryState is NotSent: the runtime never buffers unbounded work or paces your traffic. Treat it as backpressure and retry with the same key.
What goes on the wire#
Payloads serialize deterministically: model fields in schema declaration order under their 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. Unknown input properties are dropped, and a value you provide is never overridden by a default.