---
title: "Stablecoin quote API errors: what each one means and how to recover"
seoTitle: "Stablecoin quote API errors and how to recover"
description: "Every quote error a stablecoin payout API can return: rate unavailable, thin liquidity, limits, expired or used quotes, and which ones to retry."
date: "2026-10-11"
updated: "2026-10-11"
category: "payments"
author: "BlindPay Team"
faq:
  - q: "Should I retry a failed quote request?"
    a: "Only when the error is marked retryable. On BlindPay that means QUOTES_RATE_UNAVAILABLE, a rate limit, or an internal error: wait with exponential backoff and jitter, then send the same request again. Actionable errors, such as an amount outside the allowed range, fail the same way every time until you change the input."
  - q: "What does QUOTES_INSUFFICIENT_LIQUIDITY mean?"
    a: "The market could not fill the amount you asked for at that moment. It is actionable, not retryable: try a smaller amount or try again later. If you see it often on one corridor, run a quote ladder at several sizes to find where the price starts to move, and plan payout sizes around that point."
  - q: "What is the difference between QUOTES_EXPIRED and QUOTES_ALREADY_USED?"
    a: "QUOTES_EXPIRED means the quote's window passed before you executed it. QUOTES_ALREADY_USED means the quote already backed a payout, payin, or transfer. In both cases no new money moves on that quote. Request a fresh quote, show the new numbers, and execute against the new id. Never loop on the old one."
  - q: "Can I show the error description to my users?"
    a: "Yes. Every BlindPay error carries a description written to be safe for end users, with no internal or provider details. Show it, or map the code to your own copy. Keep the code and trace_id in your logs, because support needs the trace_id to find the exact request."
  - q: "Why does my quote fail with a network or token error?"
    a: "The network and token pair is not valid for that instance or that operation. Development instances quote USDB on testnets, production instances quote USDC or USDT on mainnets, and each token only exists on certain chains. The code is BLOCKCHAIN_NETWORK_NOT_SUPPORTED and it is terminal: fix the pair, don't 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](/resources/more/handle-expired-quotes-typescript-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](/changelog/2026-07-20-error-codes-new-docs-ai-assistant)). 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](/docs/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](https://google.aip.dev/194)). If you want a standard to compare against, RFC 9457 defines a generic "problem details" format for HTTP API errors ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html)). 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`:

| 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](/docs/payin-quotes) 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](/resources/more/test-offramp-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.

| 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](/docs/learn/idempotency) 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:

| `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.

## 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](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)). 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.

| 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.

## 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:

| 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](/resources/more/stablecoin-api-quotes-explained) and [how to read an FX quote](/resources/more/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](/docs/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.
