Reliability

Idempotency keys, the five delivery states, the exception taxonomy, coroutine cancellation, and bounded admission in the Repost Kotlin 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 structured concurrency and backpressure behave.

Idempotency and delivery outcomes

Repost dedupes on the idempotency key. Omit it and the runtime generates a stable key per operation and reuses it across retries; pass one (the idempotencyKey parameter on any send) to make a send safe to repeat.

Every failure is a RepostException carrying a stable errorCode, a best-known deliveryState, the idempotencyKey, and an isRetryable flag, never a raw server body or nested throwable. The five delivery states:

DeliveryStateMeaningWhat to do
ACCEPTEDRepost accepted the event.Done: you get a SendResult, not an exception.
NOT_SENTThe request definitely never left.Safe to retry with the same key.
POSSIBLY_SENTThe 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.
REJECTEDRepost definitively rejected it (a non-retryable 4xx).Fix the request; do not retry blindly.
CANCELLED_UNKNOWNYou cancelled after the request may have been sent.Same as POSSIBLY_SENT: reconcile with the same key.
try {
    val result = repost.webhooks.order.created(customerId = "customer-123") { id = "order-123" }
    println("accepted ${result.id}")
} catch (failure: RepostException) {
    when {
        // Repost may already have the event. Reuse the SAME idempotency key and
        // reconcile — never mint a new key, or you risk a duplicate delivery.
        failure.deliveryState == DeliveryState.POSSIBLY_SENT -> reconcile(failure.idempotencyKey)
        // Definitely not sent and safe to retry with the same key.
        failure.isRetryable -> retryLater(failure.idempotencyKey)
        // Terminal failure; the code is stable and carries no payload or credentials.
        else -> throw failure
    }
}

The subclass names the category: RepostConfigurationException, RepostValidationException, RepostSerializationException, RepostTransportException (connect/TLS/timeout/cancel/overload), RepostPublishException (server rejection or retryable failure), and RepostDescriptorVersionException (regenerate or upgrade).

Coroutines and cancellation

A suspend send suspends the coroutine without blocking a thread and honours structured concurrency: cancelling the coroutine cancels the underlying operation, and the coroutine sees a native CancellationException. To reconcile after a cancellation, keep the operation handle from createdOperation(...) and read its outcome(), which settles even when the send is cancelled:

val operation = repost.webhooks.order.createdOperation("customer-123", Order { id = "order-123" })
 
// Cancelling the coroutine that awaits the send also cancels this operation; here we
// cancel it directly. The delivery outcome still settles and never throws.
operation.cancel(true)
operation.outcome().thenAccept { outcome ->
    if (outcome.deliveryState == DeliveryState.CANCELLED_UNKNOWN) {
        // The request may have reached Repost. Reconcile with the same key.
        reconcile(outcome.idempotencyKey)
    }
}

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 it is exceeded, a send fails immediately with an OVERLOADED error and NOT_SENT. Treat it as backpressure, back off, and retry. The runtime never buffers unbounded work or paces your traffic.

Continue