The one rule
Branch onerror.code. Every failure is a typed string code in error.code, never a bare HTTP number.
The HTTP status is a coarse hint for proxies and logs. The contract lives in the string. Two failures can share status 422 and need opposite handling, so a switch on the number routes you wrong. A switch on error.code routes you right.
When a code carries a valid set (supported currencies, supported credentials, candidate programs),
it lives in
error.details. Read details instead of hard-coding the set. The list is per-integration
and changes as your enablement changes; the runtime source of truth is GET /capabilities.The complete table
This is the complete
error.code enum the API returns today. The set grows additively, so treat an
unrecognized code as a non-retryable failure: surface it, log it, and check GET /openapi for the
current set.The codes that carry a valid set
Four codes return the answer indetails. Read it instead of guessing.
shortfall_minor, available_actual_minor, and available_promo_minor, all minor integers. The
credential is not consumed, so the customer can top up the shortfall_minor and you can retry the
same payment.supported: the array of ISO-4217 currency codes with an active provider for this integration.candidates: the array of wallet_program_id values the credential resolves to. Re-send the call with one
of them in wallet_program_id.The supported credential types for this integration. The authoritative live source is
GET /capabilities.INSUFFICIENT_FUNDS is a recovery path, not a dead end
ModelINSUFFICIENT_FUNDS as a recovery, never a bare decline.
The credential is not consumed on this failure. A failed debit leaves the OTP retryable and the QR nonce unspent, so the same payment succeeds after a reload.
Read the shortfall
details.shortfall_minor is the exact gap. available_actual_minor and available_promo_minor
are the current balance split.Top up the gap
Credit the wallet by at least
shortfall_minor. Confirm the top-up against the provider settlement
reference with POST /topup/confirm, which credits the cashable balance and accrues any reload bonus
in one atomic operation.Retry the original payment
Retry with a NEW idempotency key (the first attempt’s key replays the failure). The credential was
never consumed, so it still resolves.
A standalone top-up entry point is not yet available. Today, confirm a top-up against a provider
settlement reference with
POST /topup/confirm. Read GET /capabilities for the operations enabled
for your integration.Retrying safely
Two failures are safe to retry with the SAMEIdempotency-Key, and the distinction matters for money.
INTERNAL_SERVER_ERROR (500)
Retry with the SAME key. If the original call committed, you get a byte-identical replay with
meta.idempotency_replayed: true. If it did not, the retry executes. Either way, no double-debit.RATE_LIMITED (429)
Back off for the seconds in
Retry-After, then retry with the SAME key. The same-key rule still
protects you against a double-debit if an earlier attempt slipped through.VALIDATION_ERROR, INSUFFICIENT_FUNDS, or IDEMPOTENCY_KEY_REUSED means the request as sent will never succeed. Fix the request, then retry under a fresh Idempotency-Key. Reusing the old key with a changed payload returns IDEMPOTENCY_KEY_REUSED.
Branching in code
Switch on the string, handle the carriers of a valid set, and treat the two retry-safe codes apart from the rest.This list is stable for the codes above, and the contract grows additively. Treat an unrecognized
error.code as a non-retryable failure: surface it, log it, and check GET /openapi for the current
set rather than crashing.Where to go next
Idempotency & Errors
The conceptual model behind the same-key replay rule and the typed-error envelope.
Conventions
The request and response envelope, headers, and the minor-units money rule.
Authentication
Resolving
INVALID_API_KEY and FORBIDDEN: keys, the terminal JWT, and tenant scope.Money: Actual vs Promotional
The balance split that
INSUFFICIENT_FUNDS.details reports.