> ## 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 C#

> Idempotency keys, the five delivery states, the RepostException taxonomy, what retries automatically, and bounded admission in the Repost C# client.

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:

```csharp theme={"languages":{"custom":["/languages/repost.json"]}}
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](/docs/send/publishing#idempotency).

## 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.                                              |

```csharp theme={"languages":{"custom":["/languages/repost.json"]}}
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.

## Continue

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

  <Card title="Configuration" icon="sliders-horizontal" href="/docs/send/csharp/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>
