Skip to main content

Retries and Ambiguous Outcomes

Outcome​

Classify the operation and certainty of its prior outcome before issuing another request. Retry budgets, timing, and operational review are client-owned policy.

What the public contract does and does not provide​

One header makes a repeat safe, and only on Order creation: POST /api/v1/orders and POST /api/v2/orders accept an optional Idempotency-Key — see Order creation below. No other operation takes it.

Beyond that the contract offers no blanket safe-repeat guarantee, no public 429 contract, and no public reconciliation operation. Label purchase is the one further documented exception to blind-repeat danger, and it is bounded — see Label purchase below.

Apart from Idempotency-Key, there is no client-supplied correlation or reconciliation identifier. The operationId and correlationId a Label-purchase 202 returns are diagnostic metadata for your own records: no public operation accepts either one as an input, so neither is a polling handle nor a way to reconcile a purchase.

One retry hint is declared, and only one: the Label-download 202 carries a Retry-After response header. It is scoped to that response and says nothing about any other operation — see Label download below.

Read-only operations​

Reissuing a read request may be reasonable after a transport failure, but the client should still apply its own retry budget and avoid unbounded loops. This does not make every GET universally safe under all conditions.

Authentication operations​

For a protected operation with an unusable access token, attempt one refresh and retry the original request once only after refresh succeeds. If refresh fails, stop and authenticate again. Never create an infinite refresh/retry loop. See Token recovery.

Refresh itself is not safely repeatable: it rotates the refresh token, so reissuing the same request with the now-spent value fails. Revoke is the opposite — repeating it for a token already revoked answers 200 again, so a revoke whose response you lost can be reissued. Neither is a blanket idempotency guarantee; see API Conventions.

Order creation​

Order creation is state-changing, so an ambiguous timeout or lost response leaves a duplicate Order possible. Both create operations accept an optional Idempotency-Key request header that removes the ambiguity: repeating a request with the same value returns the Order the first attempt created instead of creating a second one.

Send it on the first attempt — it is what a later repeat is matched against, so adding it only to the retry does nothing. Choose a new value for a genuinely new Order, and keep it to 200 characters or fewer. Values beginning with ingestion: are reserved and rejected.

Three properties are worth knowing before you rely on it:

  • The payload is not compared. A repeat under a used key returns the first Order whatever body you send with it, so a key must identify one intended Order rather than one attempt at sending.
  • The key is scoped to the account, not the sub-account. Reusing it under a different X-SubAccount-Id still returns the first Order.
  • A repeat is validated again. A retry is revalidated before the replay is looked up, so a request that is no longer valid is rejected rather than replayed.

If two requests carrying the same key are in flight at once, the second waits on nothing: if the first has already committed its Order, the second returns that Order as an ordinary replay. Only when the key is claimed but its Order is not yet readable does the second fail, with Error.ConditionNotMet and a message saying the Order is being created by another request. Retry it in a moment rather than choosing a new key.

Without an Idempotency-Key, a duplicate Order cannot be ruled out after an ambiguous outcome — do not blindly repeat the request.

Create-and-rate no-Rate 422​

Create an Order with Rates creates one Order. A no-Rate 422 may be returned after that Order has already been created, and the response carries no orderId for it.

If you sent an Idempotency-Key, this is recoverable: repeat the same request with the same key and the first attempt's Order is returned rather than a second one being created — which is how you find out whether the Order exists and what its identifier is. Without a key, do not retry the entire request or blindly repeat it; the outcome stays ambiguous.

This differs from invalid-input 422; apart from the key you supplied, the public response defines no persisted-Order identifier or recovery sequence. See Create an Order with Rates.

Rate retrieval​

Rate retrieval does not purchase a Label. A client may choose a bounded, application-owned reissue policy, but must not expect the same Rates, ordering, values, carrier availability, or eventual success.

Label purchase​

This is the one state-changing operation with a documented repeat contract, and it is the exception to the section above. There is still no idempotency key; instead, Mailhub recognizes a repeat from the Shipment's purchase operation and the request that started it. Sending the same purchase request again replays its outcome — it does not buy a second Label. A 202 means the purchase is still running, not that it failed. That response carries no retry header and defines no interval; the cadence for reading the Order back is yours.

After a timeout or lost response you therefore have two safe moves: reissue the same request within your own bounded budget, or read the Order, which carries labelId and a LabelCreated status once the purchase completes. A request that is not the same purchase is refused with 409 rather than starting a competing one.

What makes a repeat "the same purchase" is the effective request, and that is broader than the two identifiers you send. It covers the selected Rate together with the Shipment data the purchase is carried out with — the addresses, the parcel, and the carrier service the Rate resolves to. Repeating the same shipmentId and rate.id after any of that changed is therefore not guaranteed to be recognized as the same purchase, and a purchase Mailhub does not recognize as the same one is refused with 409 rather than queued behind the first.

The safe reading is narrow: a genuinely unchanged repeat replays the original outcome; anything you have edited in between is a new request you must not assume is safe. There is no public label-format input on this operation, so a format is never part of it. This is a bounded replay contract, not general retry safety and not an Idempotency-Key — this operation accepts no such header or body field; the key is offered on Order creation only. To reverse a purchase, cancel the Label; never send a different purchase request.

Two bounded conditions remain: a Label cancelled before its result reached you is not replaced by a repeat, and an unresolved carrier outcome is reconciled server-side and refuses a new purchase until it settles. See Buy a Label.

Label download​

200 is binary; 202 means pending/still generating; other documented status codes are not binary success. This 202 carries a Retry-After response header — a whole number of seconds — and it is the only response in the public contract that declares one. Wait for the interval that response gives before requesting the same download again, and read it from the header rather than hard-coding the value you saw last time.

It is a retry hint, not a completion contract: no retry budget, deadline, or eventual-success guarantee is defined, so keep the surrounding loop bounded and application-owned. See Binary Labels.

Label cancellation​

After a timeout or lost response, provider, local-state, and financial outcomes may be ambiguous. Do not blindly repeat the cancellation request. The public contract does not define universal cancellation idempotency, refund, provider-confirmation, or eligibility-window behavior.

Checklist​

  • Correct invalid input before resubmitting it.
  • Use the bounded refresh flow only for authentication recovery.
  • Keep V1/V2 creation and cancellation out of blind-repeat paths.
  • Repeat a Label purchase only as the same request, within a bounded budget.
  • Branch Label downloads by status and actual Content-Type.
  • On a Label-download 202, wait for the Retry-After value that response gives instead of an interval of your own, and keep the attempts bounded.
  • Review Authentication Errors, Validation Errors, Order State Errors, Rate Errors, and Label Errors.

Next steps​

  • Use Diagnostics to record safe evidence for an application-owned investigation.
  • Complete the Go-Live Checklist before relying on a production integration.