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
code, a legacy message, a safe description, and a trace_id.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.QUOTES_EXPIRED and QUOTES_ALREADY_USED both mean "get a new quote". Nothing moved on the old one.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 class | Meaning | What your code does |
|---|---|---|
retryable | Transient. The same request can work later | Wait, then resend the identical request |
actionable | Works only after something changes | Fix the input, or ask the user to act |
terminal | Will never work as sent | Stop 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.
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:
| Code | Retry | What it means | What to do |
|---|---|---|---|
QUOTES_RATE_UNAVAILABLE | retryable | An exchange rate was temporarily unavailable | Back off and resend |
QUOTES_INSUFFICIENT_LIQUIDITY | actionable | Not enough market liquidity to fill this amount now | Smaller amount, or try later |
QUOTES_AMOUNT_BELOW_PROVIDER_MINIMUM | actionable | Below the minimum accepted for this pair | Larger amount |
PAYOUTS_AMOUNT_BELOW_MINIMUM | actionable | Below the minimum for this payment rail | Raise the amount to the rail floor |
LIMITS_AMOUNT_OUT_OF_RANGE | actionable | Outside the allowed range; the message states the range | Read the range, adjust |
LIMITS_VOLUME_EXCEEDED | actionable | Over the customer's volume limit; the message states what's left | Lower amount, or request a limit increase |
BLOCKCHAIN_NETWORK_NOT_SUPPORTED | terminal | Invalid network and token pair for this instance or operation | Fix the pair |
CUSTOMERS_KYC_NOT_APPROVED | actionable | The customer hasn't finished verification | Complete KYC or KYB first |
BANK_ACCOUNTS_NOT_APPROVED | actionable | The destination bank account isn't approved yet | Wait for approval |
FEES_PARTNER_FEE_EXCEEDED | actionable | Your partner fee is above the allowed maximum | Lower the fee configuration |
TOS_NOT_ACCEPTED | actionable | Terms of service not accepted | Collect acceptance |
QUOTES_OTC_NOT_SUPPORTED | actionable | The OTC payin settings don't fit the OTC rules | Use 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.
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.
| Code | Retry | What it means | What to do |
|---|---|---|---|
QUOTES_EXPIRED | actionable | The window passed before execution | New quote, show new numbers |
QUOTES_ALREADY_USED | actionable | The quote already backed a transaction | New quote; check you didn't double-submit |
QUOTES_NOT_FOUND | actionable | Wrong id, or a quote from another instance | Check the id and instance |
QUOTES_INVALID_STATE | actionable | The quote can't be authorized as stored | Request 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.
Some quote rules are documented by their legacy message value. These are the ones you will see while building:
message | Where | Fix |
|---|---|---|
request_amount_must_be_a_whole_currency_unit | Payin quote, MXN, COP, or ARS on the sender side | Send a multiple of 100 minor units (whole pesos) |
funding_bank_account_id_required | ach_pull payin quote | Pass the Plaid-connected ba_... id |
funding_account_not_plaid_connected | ach_pull payin quote | Connect the account through Plaid |
cross_chain_transfers_only_support_usdc | Transfer quote across networks | Cross chains with USDC only |
cctp_route_not_supported | Transfer quote across networks | Pick a supported network pair |
kyc_not_approved | Transfer quote | Resolve 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.
Branch on the retry class first, then on the code. Three paths cover every case.
description and ask the user to act.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.
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.
| Attempt | Cap | Worst-case wait |
|---|---|---|
| Retry 1 | 1 s | 1 s |
| Retry 2 | 2 s | 2 s |
| Retry 3 | 4 s | 4 s |
| Retry 4 | 8 s | 8 s |
| Total | 1 + 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.
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:
| Code | Example 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.
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.
Seven places blockchain payments beat bank rails: contractor payroll, remittances, B2B suppliers, marketplaces, 24/7 treasury, bill pay, and PSP payouts.
Push means the payer sends an ACH transfer. Pull means you debit a connected account. How each funds a USD to USDC on-ramp, the fees, and the return risk.
AP2, ACP, and x402 each verify that an AI agent had permission to spend. Here is what every protocol covers, who backs it, and the reconciliation gap none of them close.