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-Idstill 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 theRetry-Aftervalue 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.