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. |
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():
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:
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());
}
}
}