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

> Idempotency keys, the five delivery states, the exception taxonomy, async cancellation, and bounded admission in the Repost Java client.

Every send settles in a known state, even under timeouts, cancellation, or overload. This page covers the outcome model end to end: how idempotency makes retries safe, what each delivery state means, and how cancellation and backpressure behave.

## Idempotency and delivery outcomes

Repost dedupes on the idempotency key. When you don't pass one, the runtime generates a stable key per operation and reuses it across that operation's retries. When **you** pass one via `SendOptions`, reuse the same key to make a send safe to repeat.

Every failure is a `RepostException` carrying a stable `RepostErrorCode`, a best-known `DeliveryState`, the `idempotencyKey`, the started `attemptCount`, and an `isRetryable()` flag — never a raw server body, header, or nested throwable. The five delivery states:

| `DeliveryState`     | Meaning                                                                                                              | What to do                                                                                        |
| ------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `ACCEPTED`          | Repost accepted the event.                                                                                           | Done — you get a `SendResult`, not an exception.                                                  |
| `NOT_SENT`          | The request definitely never left.                                                                                   | Safe to retry with the same key.                                                                  |
| `POSSIBLY_SENT`     | 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.                                                            |
| `CANCELLED_UNKNOWN` | You cancelled after the request may have been sent.                                                                  | Same as `POSSIBLY_SENT`: reconcile with the same key.                                             |

```java theme={"languages":{"custom":["/languages/repost.json"]}}
try {
    SendResult result = repost.webhooks().order().created("customer-123", order);
    System.out.println("accepted " + result.getId());
} catch (RepostException failure) {
    if (failure.getDeliveryState() == DeliveryState.POSSIBLY_SENT) {
        // Repost may already have the event. Reuse the SAME idempotency key and
        // reconcile — never mint a new key, or you risk a duplicate delivery.
        reconcile(failure.getIdempotencyKey());
    } else if (failure.isRetryable()) {
        // Definitely not sent and safe to retry with the same idempotency key.
        retryLater(order, failure.getIdempotencyKey());
    } else {
        // Terminal failure. Inspect the low-cardinality code; the message and
        // exception carry no payload or credentials.
        RepostErrorCode code = failure.getErrorCode();
        throw new IllegalStateException("send failed: " + code, null);
    }
}
```

The exception subclass tells you the category without a `switch`: `RepostConfigurationException` (bad config), `RepostValidationException` (your model failed validation, `NOT_SENT`), `RepostSerializationException`, `RepostTransportException` (connect/TLS/timeout/cancel/overload), `RepostPublishException` (the server returned a rejection or a retryable failure), and `RepostDescriptorVersionException` (regenerate or upgrade — the client and runtime versions are incompatible).

## Async and cancellation

`createdAsync(...)` returns a `SendOperation`, which is both a `CompletionStage<SendResult>` and a `Future<SendResult>`. Cancel it like any `Future`; the delivery outcome still settles on a separate, non-cancellable stage you read with `outcome()`:

```java theme={"languages":{"custom":["/languages/repost.json"]}}
SendOperation operation = repost.webhooks().order().createdAsync("customer-123", order);

// Cancellation is cooperative: cancel on shutdown or when the caller gives up.
operation.cancel(true);

// The result stage may finish exceptionally, but the delivery outcome always settles
// and never throws — read it to reconcile.
operation.outcome().thenAccept(outcome -> {
    if (outcome.getDeliveryState() == DeliveryState.CANCELLED_UNKNOWN) {
        // The request may have reached Repost. Reconcile with the same key.
        reconcile(outcome.getIdempotencyKey());
    }
});
```

Cancelling before the request commits settles `NOT_SENT`; cancelling after it may have been written settles `CANCELLED_UNKNOWN`. 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 an `OVERLOADED` error and `NOT_SENT` — the runtime never buffers unbounded work or paces your traffic. Treat it as backpressure:

```java theme={"languages":{"custom":["/languages/repost.json"]}}
ClientOptions options = ClientOptions.builder()
        .apiKey(System.getenv("REPOST_SEND_API_KEY"))
        .maxInFlightOperations(64)
        .build();
try (RepostClient repost = RepostClient.create(options)) {
    try {
        repost.webhooks().order().created("customer-123", order);
    } catch (RepostException failure) {
        if (failure.getErrorCode() == RepostErrorCode.OVERLOADED) {
            // The bounded in-flight budget is full. The event was NOT sent; apply your
            // own backpressure and retry — the runtime never queues past the budget.
            retryLater(order, failure.getIdempotencyKey());
        }
    }
}
```

## Continue

<Columns cols={3} className="gap-y-4">
  <Card title="Observability" icon="activity" href="/docs/send/java/observability" cta="Observe" arrow="true">
    Watch in-flight operations, dropped events, and attempt lifecycles.
  </Card>

  <Card title="Testing" icon="flask-conical" href="/docs/send/java/testing" cta="Test" arrow="true">
    Assert on outcomes and recorded requests without the network.
  </Card>

  <Card title="SDKs" icon="package" href="/docs/send/sdks" cta="All languages" arrow="true">
    The behavior every generated client shares, in every language.
  </Card>
</Columns>
