Development9 min read

ACH Return Codes Your Fintech App Needs to Handle Correctly

ACH Return Codes Your Fintech App Needs to Handle Correctly
StardelitePayment handling

ACH return codes are the reason codes banks use when they reject or reverse an ACH payment. If your fintech app moves money via ACH, you need to handle these codes correctly, or you'll frustrate users, lose money to avoidable failures, and potentially violate NACHA rules.

The most common question teams ask: which codes can I retry, and which ones mean the payment is permanently failed? The answer matters because retrying a non-retryable code wastes API calls, delays your user's experience, and in some cases breaks the rules. This guide covers the codes you'll encounter most often, how to categorize them, and what action your app should take for each.

Why ACH Return Codes Matter for Your App

When you initiate an ACH debit or credit through a provider like Plaid, Stripe, or Dwolla, the receiving bank has up to two business days to accept or reject it. If they reject it, they send back a return code explaining why. Your payment provider surfaces that code to you, usually via webhook or API response.

ACH payment processing flow

How you respond to that code determines whether the payment eventually succeeds, fails gracefully, or creates a poor user experience. Some codes indicate temporary problems that resolve on retry. Others indicate fraud flags, closed accounts, or authorization revocations where retrying makes the situation worse.

The Most Common ACH Return Codes

Here are the codes you'll see most frequently in production, grouped by how you should handle them.

R01: Insufficient Funds

The account doesn't have enough money to cover the debit. This is the single most common return code in consumer fintech.

What to do: This is retryable, but with limits. NACHA rules allow up to two retry attempts, and many apps wait 3-7 days before retrying to give the user time to add funds. Display a clear message to the user explaining the issue and when you'll retry.

R02: Account Closed

The bank account has been closed. This is not retryable.

What to do: Mark the payment method as invalid immediately. Prompt the user to add a new bank account. Do not attempt to retry. Continuing to submit transactions to a closed account can result in penalties from your payment provider.

R03: No Account / Unable to Locate Account

The account number provided doesn't exist at that financial institution, or the routing number is invalid.

What to do: Not retryable. This usually indicates a user error during account linking, or the account was closed and purged from the bank's system. Prompt the user to re-link their bank account or verify their account details.

Bank account verification interface

R04: Invalid Account Number Structure

The account number format is invalid for that bank.

What to do: Not retryable. Similar to R03, this indicates bad data at input time. If you're using Plaid Link or a similar account verification tool, you should rarely see this, as those tools validate structure upfront.

R05: Unauthorized Debit to Consumer Account

The account holder has revoked authorization for this specific debit, or claims they never authorized it.

What to do: Not retryable, and legally serious. This often indicates a dispute. You must stop all future debits to this account immediately. If you collected authorization for recurring payments, review your authorization records. NACHA requires you to retain proof of authorization for two years.

R07: Authorization Revoked by Customer

The customer has revoked authorization for all ACH debits from your company.

What to do: Not retryable. Stop all debits immediately. Unlike R05, which can apply to a single transaction, R07 typically applies to all future transactions. Mark the payment method as invalid and notify the user through your app that their authorization has been revoked.

R08: Payment Stopped

The account holder placed a stop payment order on this specific transaction.

What to do: Not retryable for this specific transaction. The user explicitly told their bank to block this payment. You can contact the user to understand why and resolve the issue, but you cannot retry the exact same transaction.

R09: Uncollected Funds

Similar to R01, but specifically means the funds are on hold or not yet available (for example, a recently deposited check that hasn't cleared).

What to do: Retryable, with the same limits as R01. Wait a few days and retry.

R10: Customer Advises Not Authorized

The customer claims they didn't authorize this transaction. This is distinct from R05 in that it's a claim of fraud or error, not just a revocation.

What to do: Not retryable. Investigate immediately. Review your authorization records and user activity logs. If this is a genuine fraud claim, you may need to involve your payment provider and potentially legal counsel.

R16: Account Frozen

The account is frozen, typically due to legal action, fraud investigation, or death of the account holder.

What to do: Not retryable in the short term. The account may eventually be unfrozen, but you have no way to know when. Treat this like R02 and prompt the user to add a new payment method.

How to Categorize Return Codes in Your Code

Your application logic should bucket return codes into three categories: retryable, non-retryable but correctable, and non-retryable terminal failures.

Retryable (with limits):

  • R01 (Insufficient Funds)
  • R09 (Uncollected Funds)

For these, implement a retry strategy with exponential backoff or fixed delays (3-7 days is common). Respect NACHA's two-retry limit. Track retry attempts in your database.

Non-retryable, user can fix:

  • R02 (Account Closed)
  • R03 (No Account)
  • R04 (Invalid Account Number)
  • R16 (Account Frozen)

For these, mark the payment method as invalid and prompt the user to add or verify a new bank account.

Non-retryable, authorization issue:

  • R05 (Unauthorized Debit)
  • R07 (Authorization Revoked)
  • R08 (Payment Stopped)
  • R10 (Not Authorized)

For these, stop all future debits immediately, flag the payment method, and depending on the code, investigate for potential fraud or disputes.

Financial dashboard showing payment status

Building Return Code Handling Into Your Payment Flow

When you receive a return code via webhook from your payment provider, here's the workflow to implement:

  1. Parse the webhook payload and extract the return code.
  2. Look up the affected payment in your database.
  3. Determine the category (retryable, non-retryable correctable, or terminal).
  4. Update payment status and user-facing state accordingly.
  5. Notify the user with clear, non-technical language explaining what happened and what they need to do.
  6. Log the event for compliance and debugging purposes.

For retryable codes, schedule a retry job. For non-retryable codes, disable the payment method and surface appropriate UI in your app.

Most payment providers (Plaid, Stripe, Dwolla, Modern Treasury) provide return code enums or constants in their SDKs. Use those rather than hardcoding strings, so you benefit from updates and catch typos at compile time.

Less Common Return Codes You Should Still Know

Beyond the top ten, there are dozens of other return codes defined by NACHA. A few worth mentioning:

  • R06 (Returned per ODFI Request): Your payment provider or sponsor bank requested the return, often due to risk or compliance issues on their end.
  • R11 (Check Truncation Entry Return): Rare, applies to specific check conversion scenarios.
  • R20 (Non-Transaction Account): The account can't be used for ACH transactions, typically a credit card account or savings account without transaction privileges.
  • R29 (Corporate Customer Advises Not Authorized): The business equivalent of R10.

If you see return codes outside the top ten frequently, it may indicate a data quality issue, a problem with your authorization flow, or a mismatch between your user base and your payment provider's capabilities.

What About Disputes and Unauthorized Transactions?

Return codes R05, R07, and R10 overlap with dispute and fraud territory. If you receive one of these, especially R10, treat it seriously. Review your ACH authorization records, your user's activity history, and your terms of service.

NACHA rules require that you obtain, verify, and retain authorization for ACH debits. If a user disputes a transaction and you can't produce proof of authorization, you'll likely lose the dispute and owe the money back, plus potential fees.

Best practice: store a timestamp, IP address, and user agent when the user authorizes ACH debits, and if you use a web authorization form, store a copy of what they agreed to. Some teams take this further and require micro-deposit verification or multi-factor authentication before allowing ACH debits.

Testing Return Code Handling

Most ACH payment providers offer a sandbox or test mode where you can simulate return codes. Plaid, Stripe, and Dwolla all support this. Use it to verify your handling logic before going to production.

Create test cases for at least R01, R02, R03, R05, and R07. Verify that your app updates state correctly, sends the right notifications to users, and logs the event for your support and finance teams.

If your provider doesn't offer return code simulation, you can still test the webhook ingestion and state machine by manually firing webhooks with mocked payloads. Tools like ngrok or webhook.site help here.

Common Mistakes Teams Make

Retrying non-retryable codes. This is the most common error. Teams see a failed payment and assume retrying is harmless. In reality, retrying R02 (Account Closed) or R05 (Unauthorized) wastes money and can trigger penalties or bans from your payment provider.

Not respecting NACHA retry limits. NACHA allows up to two retries for insufficient funds. Exceeding this can result in fines and termination of your ACH sponsorship.

Generic error messages to users. Telling a user "Payment failed" when their account is closed is unhelpful. Telling them "Your bank account is closed, please add a new one" is actionable.

Ignoring R05, R07, and R10. These codes indicate authorization problems or fraud. Ignoring them exposes you to disputes, chargebacks, and compliance risk.

Wrapping Up

ACH return codes are not edge cases. If your fintech app processes any meaningful volume of ACH payments, you'll see them regularly. The difference between a good user experience and a broken one often comes down to how you handle these codes: do you retry intelligently, surface clear explanations, and respect NACHA rules, or do you treat every failure the same and hope it resolves itself?

Building robust return code handling into your payment flow early will save you support tickets, prevent revenue loss, and keep you compliant. If you're building a fintech product and need help implementing payment flows correctly, Stardelite has experience building compliant payment systems across fintech and embedded finance. Reach out to discuss your project.

Share this: