Stablecoin quote API errors: what each one means and how to recover

Every quote error a stablecoin payout API can return: rate unavailable, thin liquidity, limits, expired or used quotes, and which ones to retry.

A stablecoin quote API error tells you one of three things: try again (the rate or market was briefly unavailable), fix something (amount, network, customer status, or bank account), or stop (the request can never work as sent). BlindPay returns a stable code, a user-safe description, and a retry class on every error, so your code can branch without parsing text.

This page is the error map for quotes. For the expiry loop itself, with code, see handling expired quotes in TypeScript and Python.

Key takeaways

  • Every BlindPay error has a code, a legacy message, a safe description, and a trace_id.
  • Retry classes: retryable (send it again later), actionable (fix the input first), terminal (don't retry).
  • QUOTES_RATE_UNAVAILABLE is the main retryable quote error. Back off with jitter.
  • QUOTES_INSUFFICIENT_LIQUIDITY is actionable: the amount was too big for the market right then.
  • At execution, QUOTES_EXPIRED and QUOTES_ALREADY_USED both mean "get a new quote". Nothing moved on the old one.

What does a quote error response look like?

A BlindPay error is a JSON body with success: false, a stable code such as QUOTES_EXPIRED, the legacy message slug such as quote_expired, a description you can show to users, an errors list for field validation, and a trace_id for support.

The code and description fields arrived in July 2026 as additive fields, so older integrations keep working (changelog). Branch on code. The message slug is kept for backwards compatibility, and several slugs can collapse into one code.

Each code also has a retry class in the API reference:

Retry classMeaningWhat your code does
retryableTransient. The same request can work laterWait, then resend the identical request
actionableWorks only after something changesFix the input, or ask the user to act
terminalWill never work as sentStop and surface the error

This is the same split Google recommends for its own APIs: retry only transient unavailability automatically, and don't retry argument or permission errors until the caller's state changes (AIP-194). If you want a standard to compare against, RFC 9457 defines a generic "problem details" format for HTTP API errors (RFC 9457). BlindPay's envelope is its own shape, but the idea is the same: a machine-readable type plus human-readable text.

Which errors can a quote request return?

A quote request fails for four reasons: the price couldn't be formed, the amount is outside a limit, the inputs don't fit together, or the customer or account isn't ready. Here are the codes you will actually meet on POST /v1/instances/{instance_id}/quotes and /payin-quotes:

CodeRetryWhat it meansWhat to do
QUOTES_RATE_UNAVAILABLEretryableAn exchange rate was temporarily unavailableBack off and resend
QUOTES_INSUFFICIENT_LIQUIDITYactionableNot enough market liquidity to fill this amount nowSmaller amount, or try later
QUOTES_AMOUNT_BELOW_PROVIDER_MINIMUMactionableBelow the minimum accepted for this pairLarger amount
PAYOUTS_AMOUNT_BELOW_MINIMUMactionableBelow the minimum for this payment railRaise the amount to the rail floor
LIMITS_AMOUNT_OUT_OF_RANGEactionableOutside the allowed range; the message states the rangeRead the range, adjust
LIMITS_VOLUME_EXCEEDEDactionableOver the customer's volume limit; the message states what's leftLower amount, or request a limit increase
BLOCKCHAIN_NETWORK_NOT_SUPPORTEDterminalInvalid network and token pair for this instance or operationFix the pair
CUSTOMERS_KYC_NOT_APPROVEDactionableThe customer hasn't finished verificationComplete KYC or KYB first
BANK_ACCOUNTS_NOT_APPROVEDactionableThe destination bank account isn't approved yetWait for approval
FEES_PARTNER_FEE_EXCEEDEDactionableYour partner fee is above the allowed maximumLower the fee configuration
TOS_NOT_ACCEPTEDactionableTerms of service not acceptedCollect acceptance
QUOTES_OTC_NOT_SUPPORTEDactionableThe OTC payin settings don't fit the OTC rulesUse the documented OTC combination

Two of these are worth a closer look.

LIMITS_AMOUNT_OUT_OF_RANGE carries the allowed range inside its message. The payin quotes docs say minimums differ by currency (COP needs a much higher raw minimum in minor units because of the peso's small nominal value), so never hardcode one floor across currencies. Read the error.

QUOTES_INSUFFICIENT_LIQUIDITY is the only error that tells you something about the market itself. It is the API saying "this size, right now, doesn't fill". How to test an off-ramp's liquidity depth shows how to find that size on purpose before production finds it for you.

Which errors show up when you execute a quote?

Execution errors are about the quote's state, not the market. The price was already locked, so the only questions are whether the quote is still valid and whether it was already used.

CodeRetryWhat it meansWhat to do
QUOTES_EXPIREDactionableThe window passed before executionNew quote, show new numbers
QUOTES_ALREADY_USEDactionableThe quote already backed a transactionNew quote; check you didn't double-submit
QUOTES_NOT_FOUNDactionableWrong id, or a quote from another instanceCheck the id and instance
QUOTES_INVALID_STATEactionableThe quote can't be authorized as storedRequest a new quote

QUOTES_ALREADY_USED deserves a second look in your logs. It often means a retry fired without an Idempotency-Key, so the first attempt succeeded and the second hit a spent quote. The idempotency docs explain the header: same key and same body bytes replay the original response instead of running again.

What do the snake_case errors in the quote docs mean?

Some quote rules are documented by their legacy message value. These are the ones you will see while building:

messageWhereFix
request_amount_must_be_a_whole_currency_unitPayin quote, MXN, COP, or ARS on the sender sideSend a multiple of 100 minor units (whole pesos)
funding_bank_account_id_requiredach_pull payin quotePass the Plaid-connected ba_... id
funding_account_not_plaid_connectedach_pull payin quoteConnect the account through Plaid
cross_chain_transfers_only_support_usdcTransfer quote across networksCross chains with USDC only
cctp_route_not_supportedTransfer quote across networksPick a supported network pair
kyc_not_approvedTransfer quoteResolve the customer's status first

The whole-peso rule is the one that surprises people. The payin quotes docs explain why: those rails settle in whole units, so a fractional amount would be truncated on arrival.

How should your code decide what to do with an error?

Branch on the retry class first, then on the code. Three paths cover every case.

  1. Retryable. Resend the same request with exponential backoff and jitter. AWS's analysis of retry storms shows that backoff alone still bunches clients together, and adding randomness spreads them out (AWS Architecture Blog). Cap the attempts.
  2. Actionable. Don't retry blindly. Fix the input yourself (amount, network pair, bank account) or show the description and ask the user to act.
  3. Terminal. Stop, log the trace_id, and alert a human if it shouldn't happen in production.

One idempotency detail matters here. BlindPay stores only 2xx and 4xx JSON responses for replay. A 5xx releases the key, so a retry with the same key runs as a fresh attempt instead of replaying the failure. That's exactly what you want for QUOTES_RATE_UNAVAILABLE.

Illustrative example: how long can a retry budget take?

Say a quote request returns QUOTES_RATE_UNAVAILABLE. You retry with "full jitter": wait a random time between 0 and the cap for that attempt, where the cap doubles from 1 second up to 8 seconds.

AttemptCapWorst-case wait
Retry 11 s1 s
Retry 22 s2 s
Retry 34 s4 s
Retry 48 s8 s
Total1 + 2 + 4 + 8 = 15 s

Worst case, four retries add 15 seconds, and the average with full jitter is about half that, 7.5 seconds. No quote exists yet, so no 5-minute window is burning. After the fourth failure, stop and tell the user the rate is briefly unavailable, using the description text.

The same budget would be wrong at execution. If POST /payouts/evm returns QUOTES_EXPIRED, retrying the identical request fails forever. The fix is a new quote, which is a new request with a new idempotency key.

Which error messages are safe to show users?

The description field is written to be safe for end users and never names a third-party provider. You can show it as is. Most teams still map the common codes to their own copy, so tone matches the product:

CodeExample copy
QUOTES_RATE_UNAVAILABLE"We couldn't get a live rate. Trying again..."
QUOTES_INSUFFICIENT_LIQUIDITY"This amount can't be filled right now. Try a smaller amount."
QUOTES_EXPIRED"This price expired. Here is the updated amount."
LIMITS_VOLUME_EXCEEDED"You've reached your limit for now."

Keep the raw code and trace_id out of the UI and in your logs. The quote fields explained and how to read an FX quote help when the error is really a misread amount or rate.

What to do next

Open the API reference, list the codes for POST /quotes and POST /payouts/evm, and add a switch on the retry class to your quote client before the first production payout.

FAQ