A stablecoin quote API error is a failed response to a price request, or to a payment that tries to spend a quote. Each one tells you to do one of four things: fix the request, ask for a new quote, retry after a pause, or stop. Branch on the code and the HTTP status, never on the message text.
The category is defined in what a stablecoin API is, the build is in the seven-step integration guide and post-submission rejections are in why a stablecoin payout gets rejected. The error codes, the HTTP statuses and the field names below come from the Lumx errors page and the API reference, read on November 1, 2026.
The shape of a Lumx error response
Every error from the API has the same body: requestId, timestamp, path, status, code and message, plus a validationErrors list on a VALIDATION_ERROR. One code path can handle all of them. The message text can change, and on any 5xx it is always "An unexpected error occurred.", so the code is the only field worth branching on. Keep the requestId with every failed call.
The catalog is not exhaustive, so an unknown code needs a default: log it with the requestId, treat it as not retryable, and surface it to a person.
Which errors can come back when you ask for a price?
These are the codes that can come back when you ask for a price, grouped by our reading of the catalog descriptions. The docs do not map codes to routes, and we did not run each one in sandbox, so treat the grouping as a guide, not a contract. The likely reasons are a malformed body, an amount outside what the pair supports, no price formed, or a rail or customer that is wrong for the request. The request is POST /exchange-rates with a customerId, a type (FLOATING or LOCKED), a rail, source and target currencies and an amount. The Idempotency-Key header is optional on this route, per the reference, so a quote call can go without one. The sandbox is open to self-serve, has every currency enabled and rejects an invalid destination, so reproduce each code there against the pairs you serve before you rely on it.
Errors you meet when asking for a price, with the HTTP status from the catalog and the action we recommend.
Code | HTTP | What the catalog says | Action |
| 400 | Request body failed validation, details in | Fix the request |
| 422 | Amount is below the supported minimum for the currency | Fix the amount |
| 422 | No liquidity available for this trade | Smaller amount, or retry later |
| 500 | Fetching or locking the rate failed | Retry with backoff, then stop |
| 403 | Rail is not enabled for this project | Stop, ask Lumx to enable it |
| 404 | Customer does not exist | Fix the |
Two of these deserve a closer look. MINIMUM_AMOUNT_NOT_MET depends on the currency, so one hardcoded floor across pairs will be wrong somewhere. Keep one floor per currency in configuration, so the form blocks the amount before the call. NO_LIQUIDITY_PROVIDER says something about the size of the trade at that moment. It is a reason to try a smaller amount or a later time, not to loop. If a corridor returns it often, split the run into smaller payments.
A successful quote response carries the id you later pass as exchangeRateId. The pairs and rails you can quote are on the coverage page and in the global payments product overview.
Errors when a quote is spent
A locked quote exists to be passed as exchangeRateId on POST /transactions/on-ramp or POST /transactions/off-ramp. Errors here are about the state of the quote and of the customer, and the price is already fixed, so retrying the identical call rarely helps. The four EXCHANGE_RATE_* codes concern locked quotes. A FLOATING rate has no ID to pass and no expiry, so those codes should not apply to it. The docs do not state this outright, so check it in sandbox.
Errors you meet when a payment uses a quote.
Code | HTTP | What the catalog says | Action |
| 422 | Locked rate has expired | Ask for a new quote |
| 422 | Rate was already consumed by another transaction | New quote, and check for a double submit |
| 404 | Exchange rate does not exist | Fix the ID |
| 422 | Rate currency does not match the transaction type | New quote for the right direction |
| 422 | Trade cannot be settled at the moment | New quote after a short wait |
| 422 | Balance is too low for the operation | Fund the wallet, then re-quote |
| 422 | Exceeds the customer's single, daily or monthly limit | Lower the amount, or request a higher limit |
| 403 | Customer verification (KYC, the check on an individual, or KYB, the check on a business) is not approved | Finish verification first |
EXCHANGE_RATE_EXPIRED is the normal one. The lock is a window of 30 seconds, 1 minute or 5 minutes, chosen with the timelock field and set to 30 seconds when you leave it out. The clock starts when the quote is created, and the response carries an absolute expiresAt timestamp in UTC. Drive the countdown from expiresAt rather than from a local timer, so clock drift on the client cannot keep a dead quote on screen. The old ID is dead: request a new quote, which comes back with a new exchangeRateId, show the new amount, and execute against the new ID. Asking again is safe, because a locked quote that is never used leaves nothing behind. One exception to the expiry: a transaction held for an RFI (request for information from compliance) still settles at the quoted rate after the lock lapses, because the fiat leg was already converted at the agreed rate.
EXCHANGE_RATE_ALREADY_USED is worth a look in your logs. A quote backs one transaction, so this code often means the first attempt worked and a second attempt fired. Before you request a replacement, read the transaction list for that customer. The limit codes follow the transaction limits page: the thresholds are global per customer and transaction type (onramp, offramp, transfer), each with a single, daily and monthly cap, and they do not change by rail. The cut-offs that do apply by rail are in payout limits and cut-offs by rail.
What Lumx does at this point: a locked rate past expiresAt is no longer valid, and the docs tell you to request a new rate to continue. A locked on-ramp expires together with its quote, and a deposit that lands after that is refunded to the sender on the same rail.
Errors that can hit any call
Some failures have nothing to do with the quote. They come from the key, the rate limit, the provider side or the idempotency header, and cover every route.
Errors that apply to quote and payment calls alike.
Code | HTTP | What the catalog says | Action |
| 429 | Rate limit exceeded | Retry with exponential backoff |
| 503 | Provider call timed out, safe to retry | Retry (on a spend call, check first) |
| 503 | Network failure reaching the provider, safe to retry | Retry (on a spend call, check first) |
| 503 | Provider is unavailable, safe to retry | Retry (on a spend call, check first) |
| 500 or 502 | An internal dependency failed | Retry with backoff, then stop |
| 400 | Provider rejected the request data, not retryable | Fix the request |
| 409 | A request with that key is still processing | Wait, then read the result |
| 409 | Same key reused with a different body | New key |
| 401 | API key is missing, invalid or inactive | Fix the key |
| 403 | API key lacks the scope required by the route | Fix the key scope |
The three 503 codes carry "safe to retry" in the catalog, which does not say what state a transaction is in when one comes back. On a quote request that is harmless. On a spend call, read the customer's transaction list before you retry with a new key. A locked quote is protected, because a second spend returns EXCHANGE_RATE_ALREADY_USED. A payment with a floating rate has no such guard, so a new key after a 5xx risks a double payout. INVALID_REQUEST is the one that says not retryable. The rest are our recommendation.
Retry, re-quote, fix or stop: one rule per code
Fix when the request itself is wrong. Re-quote when the price or its lock is the problem. Retry when the provider side hiccupped. Stop when only a person can change the outcome, such as a key scope or a verification status.
The idempotency key changes the retry rule, and it is the part clients get wrong. The docs say the API stores the response for a key for 24 hours and that both successful and failed responses are cached: if the original request failed, a retry with the same key returns the same error. So after a 4xx, the retry takes a fresh UUID v4. After a 5xx on a spend call, read the transaction list first, then decide. Reuse the key only when no response arrived at all, and only on a call that spends money. A quote request can take a new key every time. Key mechanics are in idempotency keys for a stablecoin API.
A short retry budget makes the lock window concrete. Illustrative example: a 30-second lock, and an execute call that returns TOO_MANY_REQUESTS. Backing off for 1, 2 and 4 seconds adds up to 7 seconds in the worst case, which leaves 23 seconds of the lock for the call that succeeds. If the lock runs out first, the next error is EXCHANGE_RATE_EXPIRED, and the answer to that is a new quote, not another retry.
What to log and alert on
I would log the code, the HTTP status and the requestId on every failed quote, and I would alert on the share of each code, not on the raw count of errors. A rising share of NO_LIQUIDITY_PROVIDER or MINIMUM_AMOUNT_NOT_MET tells you your payment sizes are drifting out of what a corridor supports, which is a product problem. A rising share of EXCHANGE_RATE_FETCH_FAILED or a 503 code is a provider incident. One dashboard that mixes them hides both. If a payout still comes out short, why a recipient got less traces the gap.
A quote error is synchronous. A payment that fails after submission arrives as an event, such as offramp.failed; see stablecoin API webhooks.
When retrying is the wrong response
Retries fit transient failures only. Four cases go the other way:
A 400, 403, 404 or 422 on the same body. These fail identically each time. A 429 is the exception, and
NO_LIQUIDITY_PROVIDERis the one 422 to retry later with a smaller amount.An expired lock. The ID is dead, so the same call returns the same error.
A spend call with a new key after a lost response. This is how one payout becomes two. Reuse the stored key and read the transaction before sending again.
A quote error shown to a customer as "try again". For a minimum or a limit, the customer needs a different amount.
What a quote error looks like on a Pix payout with Lumx
Take a marketplace that pays a seller in Brazil over Pix (Brazil's instant payment system, run by the Central Bank), with the reais shown before approval. Lumx is stablecoin payments infrastructure for businesses that move money between Latin America and the rest of the world: one API to collect, hold, convert, and pay out in BRL, MXN, COP, USD, EUR, and GBP or in USDC and USDT, over local rails such as PIX, SPEI, PSE, Bre-B, ACH, FEDWIRE, SEPA, and Faster Payments, with SWIFT and on-behalf-of payments and collections (POBO and COBO) in USD, EUR, and GBP, plus named virtual accounts, custodial wallets, and KYB/KYC built in.
At the confirm step the platform sends POST /exchange-rates with type: LOCKED, rail: PIX, a USDC source amount, BRL as the target and a timelock of 1m. The docs example shows BRL to USDC, and we did not run this USDC to BRL request in sandbox, so check the request body against the reference. A bad body returns VALIDATION_ERROR with the field named. An amount under the floor returns MINIMUM_AMOUNT_NOT_MET, and the form should have caught it first. A good request returns an id, an expiresAt and the amounts after fees. On approval the platform sends POST /transactions/off-ramp with the destinationId, the exchangeRateId and a purpose, plus an Idempotency-Key it saved before sending. A late tap returns EXCHANGE_RATE_EXPIRED, and the platform requests a new quote with a new key. The error shape and catalog do not change by rail. In sandbox, an on-ramp follows its normal flow and a deposit can be simulated, so you can walk the whole path first.
Pricing around the quote is a monthly platform fee plus usage-based per-transaction fees, transparent FX on locked quotes, no capital parked in settlement accounts beforehand; no published plans. The fee lines you see in a quote response are explained in how stablecoin payout pricing works. New codes are announced in the Lumx changelog.
Methodology and sources
Every code and status above, and every field name, was read from the public Lumx docs on November 1, 2026: errors, exchange rates, the get an exchange rate reference, the off-ramp reference, transactions and idempotency. The lock durations and the default come from the exchange rate request schema. The catalog describes each code but does not map codes to routes, so the split into quote errors and spend errors is our reading of the descriptions. We reproduced none of the codes in sandbox for this post, so every code is documented, not tested. The retry recommendations are ours except where a row says the catalog marks the code safe to retry or not retryable.
Verified on November 1, 2026. Operational context, not legal, tax, or investment advice.
Cover photo: Markus Kammermann on Unsplash.
What are stablecoin quote API errors?
Stablecoin quote API errors are the failed responses you get when you ask for a price, or when a payment tries to use one. Each one maps to a fix, a new quote, a retry or a stop. On Lumx every error has the same body with a code, a status and a requestId, and you branch on the code, because the message text can change.
Which stablecoin quote errors can I retry?
Retry a 429 with exponential backoff, and the 503 codes TIMEOUT_ERROR, NETWORK_ERROR and SERVICE_UNAVAILABLE, which the Lumx catalog marks safe to retry. Do not retry a 400, 403, 404 or 422 on the same body; a 429 is the exception, and NO_LIQUIDITY_PROVIDER is the one 422 to retry later with a smaller amount. After a 4xx, send the retry with a new idempotency key. After a 5xx on a payment, read the customer's transaction list first, because the docs mark the 503 codes safe to retry without saying what state the transaction is in.
Why does my quote fail with a minimum amount or no liquidity error?
MINIMUM_AMOUNT_NOT_MET means the amount is below the minimum for that currency, so raise it or block it in the form. NO_LIQUIDITY_PROVIDER means no liquidity was available for that trade at that moment, so try a smaller amount or a later time. Both return HTTP 422 in the Lumx catalog. Lumx publishes both codes, so a client can tell a floor from a liquidity gap.
Why does a retry return the same error?
The idempotency key stores the response, and failed responses are stored too. If the first call returned an error and you retry with the same key and body, you get the same error back. Use a new key after an error response, and reuse the key only when no response arrived. Lumx keeps keys for 24 hours and returns a 409 if the same key arrives with a different body.
What should I send to support when a quote fails?
Send the requestId, the code, the status, the timestamp and the path from the error body. The requestId lets support find the exact call, and on a 5xx the message is always generic, so the code carries the detail. Lumx puts all five fields in every error response and asks for the requestId on support requests.





