Em processo de adequação ao regime das SPSAV, nos termos da Resolução BCB nº 520/2025 (regime de transição do art. 88)

  • Em processo de adequação ao regime das SPSAV, nos termos da Resolução BCB nº 520/2025 (regime de transição do art. 88)

Guides

Why a stablecoin payout gets rejected, and what each reason code means

A payout can be refused at four different moments, by four different parties. What each rejection means, which ones are retryable, and which are yours.

Caio Barbosa

Fundador & CO-CEO

Forbes Under 30. Uma das principais vozes em Fintech & Crypto no Brasil. Escreve semanalmente sobre stablecoins, pagamentos e o futuro da infraestrutura financeira na América Latina.

Cover image for Lumx blog article: Why a stablecoin payout gets rejected, and what each reason code means
Cover image for Lumx blog article: Why a stablecoin payout gets rejected, and what each reason code means

A rejected stablecoin payout is almost never one thing: the refusal can come from the API before the payment exists, from a compliance hold after it exists, from the receiving bank's validation of the beneficiary, or from the destination institution after the money has already left. Those four are different events with different owners and different fixes, and a support process that treats them as one category produces the worst possible answer to a customer, which is that the bank rejected it.

The useful discipline is to know which of the four you are looking at before deciding what to do. The rest of this post is how to tell them apart, and what is published about each one. The mechanics of the payout itself are covered in what a stablecoin off-ramp is.

The four places a payout can be refused

The first is synchronous. You call the endpoint, the request never becomes a transaction, and you get an HTTP status with a machine-readable code in the body. This is the best case, because it is immediate and specific.

The second is a compliance hold. The transaction exists and stops, either because the customer is over a limit or because monitoring flagged it. Nothing is wrong with your code.

The third is validation at the rail. The beneficiary details are syntactically fine and the receiving institution refuses them anyway, usually because a tax ID does not match the account holder or the account is closed.

The fourth is failure after the money left our banking partner. The transaction reaches a terminal failed state, and the route to an explanation runs through support rather than through your own code.

Refused at the API: the codes worth handling by name

Every error uses the same envelope: a request identifier, a timestamp, the path, the HTTP status, a machine-readable code and a human-readable message. Match on the code. The documentation is explicit that the message is subject to change, and that on any 5xx response the message is always the same generic sentence while the code stays specific.

Codes a payout integration should handle by name, from the published error catalog

Code

Status

What it means

Whose fix

KYC_NOT_APPROVED

403

Customer verification is not approved

Yours, or the customer's

ACCOUNT_NOT_ACTIVE

409

The account is not in a usable state

Wait, or resolve a hold

WALLET_NOT_FOUND

404

No wallet on the requested blockchain

Yours: wrong network

DESTINATION_NOT_FOUND

404

The destination does not exist

Yours

RAIL_NOT_ENABLED

403

The rail is not enabled for the project

Ours: ask for it

HOLDER_TYPE_MISMATCH

403

Holder type differs from the customer type on a self destination

Yours

HOLDER_TAX_ID_MISMATCH

403

Holder tax ID differs from the customer tax ID on a self destination

Yours

PERSONAL_ACCOUNT_RELATIONSHIP_REQUIRED

400

A personal transaction needs a self destination

Yours

INSUFFICIENT_BALANCE

422

The balance is too low

The customer's

TRANSACTION_LIMIT_EXCEEDED

422

Over the single, daily or monthly limit

Compliance

MINIMUM_AMOUNT_NOT_MET

422

Below the supported minimum for the currency

Yours

INVALID_TAX_ID

400

The tax ID was rejected by bank validation

The beneficiary's

INVALID_AMOUNT

400

The amount was rejected by bank validation

Varies

INVALID_PAYMENT_DESTINATION

400

The payment destination is invalid

Yours

EXCHANGE_RATE_EXPIRED

422

The locked rate expired before submission

Yours: re-quote

EXCHANGE_RATE_ALREADY_USED

422

The rate was consumed by another transaction

Yours

NO_LIQUIDITY_PROVIDER

422

No liquidity available for the trade

Ours

INSOLVENT_TRADE

422

The trade cannot be settled at the moment

Ours

IDEMPOTENCY_KEY_CONFLICT

409

The same key was reused with a different body

Yours

TOO_MANY_REQUESTS

429

Rate limited

Yours: back off

Two of those deserve a second look. HOLDER_TAX_ID_MISMATCH and PERSONAL_ACCOUNT_RELATIONSHIP_REQUIRED are not validation pedantry: they enforce that a payment to the customer's own account is actually to the customer's own account, which is the line between a payout and an undisclosed third-party transfer.

The catalog states that it covers the most common codes and is not exhaustive, so the correct client behaviour is to branch on the codes you know and fall through to a generic handler for the rest. A client that switches exhaustively on this list will break the first time a code is added.

Retryable, not retryable, and the difference between them

The service availability group is the only one where retrying is the documented answer. TIMEOUT_ERROR, NETWORK_ERROR and SERVICE_UNAVAILABLE all return 503 and are all marked safe to retry. INVALID_REQUEST returns 400 and is explicitly not retryable, because the provider rejected the data itself.

Retry with the same idempotency key, always. A retry with a new key is a second payment waiting to happen, and reusing a key with a different body returns IDEMPOTENCY_KEY_CONFLICT rather than quietly doing something surprising. If a request with that key is still processing you get IDEMPOTENCY_KEY_IN_FLIGHT, which means wait rather than escalate.

TOO_MANY_REQUESTS is the rate limiting code, and the right behaviour on a 429 is exponential backoff rather than a request budget computed in advance. Pacing a client against an assumed threshold is guessing with extra steps, and the assumption is the part that breaks first, usually on the day a batch gets bigger.

Refused by compliance: limits and holds

Limits are a property of the customer and of how far verification went, not of the rail. KYB (know your business, the verification of a company and its owners) and KYC (know your customer, the verification of an individual) each have a standard tier and an enhanced tier, and the published ceilings differ per transaction, per day and per month. Reading the customer with transaction limits included returns the used and remaining amounts against both the daily and monthly ceiling, which is what lets a product warn before the payment rather than after the rejection. The numbers themselves are in payout limits and cut-offs by rail.

A hold is different. When transaction monitoring flags an off-ramp, the transaction pauses in an RFI (request for information, a compliance hold that asks for a document before a transaction clears). For transactions this is handled by our compliance team on email or Slack rather than through an API, and the questions are consistent: the relationship between sender and receiver, the purpose of the payment, and what outcome is expected. The documented consequence of silence is blunt: if nobody responds within 24 hours, the transaction may be refunded to the sender.

That deadline is the reason the purpose field matters more than it looks. Every off-ramp carries a purpose value, banking partners use it for screening and reporting, and a mismatch between the stated purpose and what the payment obviously is can hold the transaction or trigger the RFI in the first place.

Refused by the rail: the beneficiary is the usual cause

INVALID_TAX_ID and INVALID_AMOUNT are both described as rejections by bank validation, which is the API telling you that the refusal came from outside. In Brazil this is typically a CPF (the Brazilian individual taxpayer ID) or CNPJ (the Brazilian company taxpayer ID) that does not belong to the holder of the destination account, and the fix is on the beneficiary's side rather than in your code. Collecting the tax ID and the account holder name together, and validating them against each other before the first payout rather than during it, removes most of this category. That is the same habit that makes sending USDC to Brazil boring.

Relationship is the other rail-adjacent rejection. Every destination declares how the account holder relates to the customer, and the accepted values cover self payments plus a dozen third-party relationships, from supplier and employee to creditor and friend. That declaration is not paperwork: it is what lets the payment be screened as what it actually is, and a wrong value produces either a rejection now or a question later.

After it left: what the failure event carries

When an off-ramp fails after submission, the transaction moves to a terminal failed state and a failure event is delivered. The event carries the transaction identifier, the request you originally sent, the terminal status and a message directing the reader to support.

That is the point where the answer stops being something your code computes and becomes something you ask for. Keep the request identifier from the original call and the transaction identifier from the event, and open the conversation with both. Together they identify one payment, which is the difference between a question that can be answered and a search.

Design the customer-facing side for this case on its own terms. A failure after submission is a different state from a refusal on submission, and a product that renders both as failed with the same copy leaves its own support team nothing to work from. Record which of the two happened against your order reference, and store both identifiers next to it.

When a rejection is not a bug in your integration

A limit rejection on an approved customer is the system working. The fix is a limit increase request with a supporting document, reviewed against a published target of one business day, and it belongs to the customer's growth rather than to your code.

A compliance hold is also not a defect. Holds exist because the alternative is losing the banking relationships the corridor runs on, and an operation that treats every hold as an incident will burn its own credibility on the day one is real.

And a rail refusal for a mismatched tax ID is the rail doing the job. What you can control is whether your product discovers it during onboarding, when the customer is present and can fix it, or during a payout, when they are not. If you are rejecting a material share of payouts on beneficiary data, that is a form design problem rather than a payments problem, and what a stablecoin corridor is is worth reading before adding a second one.

The discipline I care about most in this whole area is what a support team is allowed to say. "The bank rejected it" is rarely true and constantly said, because it is the shortest sentence that ends the conversation. My rule is that a payments product owes its user the moment of the refusal and the owner of the fix, even where it cannot name the cause: refused before the payment existed, held for review, refused by the receiving institution, or failed after leaving. Telling a customer which of those four happened costs nothing and changes the whole tone of the exchange, and a team that cannot tell them apart internally is never going to tell them apart to a customer.

Reading a rejection on a payout with us

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

A payout that is going to be refused synchronously is refused before a transaction exists, which means there is nothing to reconcile and nothing to reverse. The response carries the request identifier, and that identifier is the thing to quote to support; it is worth persisting alongside your own order reference even for calls that succeeded.

A payout that is accepted moves through its states and emits an event on each one, so a hold shows up as a transaction that stopped rather than as silence. Which rails are live, and therefore which RAIL_NOT_ENABLED responses are a configuration question rather than a coverage question, is published on the rail coverage page, and how the pieces fit per country is in global payments.

Methodology and sources

Every code and behaviour quoted here is from the published Lumx error catalog, the request for information page, the transactions page and the transaction limits page, all read on September 24, 2026, plus the production OpenAPI specification downloaded the same day. Where the documentation states a target rather than a commitment, this post says target.

The catalog this post draws on states that it is not exhaustive, so the table above is the set worth handling by name rather than the complete set, and a client that falls through on anything unfamiliar will keep working as codes are added. Nothing here is reconstructed from memory: where the documentation states a code, this post states the same code, and where it describes a behaviour, the wording follows it.

Verified on September 25, 2026. Operational context, not legal, tax, or investment advice.

Cover photo: Bergstrand Consultancy on Unsplash.

  • What is the difference between a rejected request and a failed transaction?

    A rejected request never becomes a transaction: the API refuses it synchronously and returns a specific code you can branch on. A failed transaction was accepted, moved through its states and ended in a terminal failure after the money had already started moving. The first is usually fixable in your own code, the second usually needs support with the transaction identifier.

  • Which payout errors are safe to retry?

    The service availability group: timeouts, network errors and provider unavailability all return 503 and are documented as retryable. A provider rejection of the request data returns 400 and is documented as not retryable, so retrying it only produces the same answer. Always retry with the original idempotency key rather than a new one.

  • Why did a payout stop without any error at all?

    That is usually a compliance hold rather than an error. The transaction exists, its status stopped advancing, and our compliance team reaches out on email or Slack for the relationship, purpose and expected outcome. Responding within a day matters, because an unanswered hold can end in the funds being returned to the sender.

  • How do we stop beneficiary data from causing rejections?

    Collect the tax ID and the account holder name together, and validate that they match before the first payout rather than during one. Most rail-level refusals in Brazil are a tax ID that does not belong to the holder of the destination account, which is a rejection the receiving institution makes and nobody upstream can override. Catching it at onboarding turns a failed payment into a form error.

Fique por dentro do que a Lumx está desenvolvendo.

Inscreva-se para recebê-los por e-mail.

Compartilhe nas redes sociais:

why-stablecoin-payouts-get-rejected

A

why-stablecoin-payouts-get-rejected

Why a stablecoin payout gets rejected, and what each reason code means

Copiar link

Copiado!

why-stablecoin-payouts-get-rejected

FALE COM NOSSO TIME

Pronto para transformar seu negócio com stablecoins?

Descubra como nossa infraestrutura pode integrar stablecoins às suas operações financeiras de forma rápida, segura e eficiente.

Guides

Nesta página

©2026. Todos os direitos reservados.

A LUMX SOCIEDADE PRESTADORA DE SERVIÇOS DE ATIVOS VIRTUAIS LTDA., pessoa jurídica de direito privado, inscrita no CNPJ/MF sob o nº 42.887.120/0001-00, (“Lumx”) atua como prestadora de serviços de ativos virtuais e encontra-se em processo de adequação ao regime regulatório das Sociedades Prestadoras de Serviços de Ativos Virtuais (SPSAV), nos termos da Resolução BCB nº 520/2025, estando atualmente sujeita ao regime de transição previsto em seu art. 88.

A Lumx não é banco, instituição financeira, instituição de pagamento ou custodiante de recursos de clientes. Determinados serviços disponibilizados por meio da Plataforma poderão ser prestados por parceiros terceiros devidamente autorizados e regulados, nos termos da legislação aplicável.

Consulte os Termos de Uso e o Aviso de Privacidade da Lumx para obter mais informações sobre as condições de utilização da Plataforma e o tratamento de seus dados pessoais.