error.code is a stable machine string. Build your logic against it and
against the three booleans — never against the HTTP status alone, and never
by string-matching message. Message text can change without notice;
error.code will not.
The three booleans
Exactly one of the three is always true. This is the whole contract for safe retries.terminal: true — a definitive outcome
terminal: true — a definitive outcome
The request reached a final answer. Retrying with the same idempotency key
cannot change it.What to do: if it was a debit-adjacent operation, reverse your own
side and surface the failure to your user. Do not retry.
retriable: true — nothing moved
retriable: true — nothing moved
Safe to retry. Nothing has been applied on our side or the provider’s.What to do: retry with backoff, using the same idempotency key.
Never mint a new key for a retriable failure — a new key restarts the
whole operation as if it were unrelated.
ambiguous: true — genuinely unknown
ambiguous: true — genuinely unknown
The outcome is unknown. The request may or may not have been applied by
the provider.What to do: do not reverse anything. Poll
GET /v1/collections/{reference} or GET /v1/payouts/{reference} until it
resolves to a terminal status, or escalate to manual review after a
reasonable timeout. Reversing an ambiguous outcome is exactly how a
double-processing or a lost debit happens.Handling the three cases
handle-error.ts
Common codes
A few worth calling out:
insufficient_tenant_float— your float balance in that currency does not cover the amount plus the estimated fee. Top up; the request itself was well-formed.velocity_limit_exceeded— you crossed a per-hour, per-day or per-destination cap. Retriable, but retrying immediately will fail again; back off meaningfully.idempotency_key_reused— the same key with different parameters. This is a bug or an attack on your side, never a legitimate retry. See Idempotency.mode_mismatch— your body claimed a mode that disagrees with the key you signed with. Mode comes from the key. See Modes.