DEV Community

Payout Rail
Payout Rail

Posted on

ACH Return Codes Explained: R01–R85 and How to Handle Them in Production

ACH Return Codes Explained: R01–R85 and How to Handle Them in Production

ACH Return Codes Explained: R01–R85 and How to Handle Them in Production

When an ACH transfer fails, your payout system receives a return code. Understanding what each code means—and when it arrives—is critical to building reliable payment flows. This guide maps the Nacha standard return codes to real developer scenarios and shows you how to decode them programmatically.

Why ACH Returns Matter

ACH transactions settle in 1–2 business days, but returns can arrive up to 5 business days later. Unlike credit cards, where declines happen in milliseconds, ACH returns are asynchronous. Your code must:

  • Store the original transaction state
  • Match the return to the original entry
  • Decide: retry, escalate, or route to an alternate rail
  • Update the user and reconcile accounting

The Major ACH Return Codes

The National Automated Clearing House Association (Nacha) defines 86 return codes (R01–R85, plus R99 for vendor errors). Here are the ones you'll encounter most:

Code Meaning Cause Action
R01 Insufficient Funds Account balance too low Retry after 1–2 days or escalate
R03 No Account / Account Closed Account doesn't exist or is closed Mark as permanent failure; notify user
R04 Invalid Account Number Structure Routing or account format wrong Validate before retry; check data entry
R05 Account Closed by Institution Bank closed the account Permanent failure; request new account
R07 Authorization Revoked User or bank revoked permission Permanent failure; re-verify mandate
R10 Customer Advises Unauthorized User disputes the transaction Investigate; may need manual review
R20 Non-Transaction Account Account type incompatible with ACH Retry with different account type or rail
R29 Corporate Customer Advises Not Authorized Business account holder disputes entry Escalate for manual investigation

Handling R01: Insufficient Funds

R01 is the most common return. The receiver's account lacked funds at settlement time—but may have funds tomorrow.

async function handleAchReturn(returnCode, transaction) {
  if (returnCode === 'R01') {
    // Insufficient funds: retry once after 2 business days
    const retryDate = addBusinessDays(new Date(), 2);

    await updateTransaction(transaction.id, {
      status: 'pending_retry',
      nextRetryAt: retryDate,
      returnReason: 'Insufficient funds',
      attemptCount: (transaction.attemptCount || 0) + 1
    });

    // Notify user
    await notifyUser(transaction.userId, {
      subject: 'Payment pending – will retry',
      message: `We'll try again on ${retryDate.toDateString()}`
    });

    return { action: 'retry', retryDate };
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling R03 / R05: Account Doesn't Exist

R03 (no account) and R05 (account closed) are permanent failures. The account is gone or invalid.

async function handleAchReturn(returnCode, transaction) {
  if (['R03', 'R05'].includes(returnCode)) {
    // Permanent failure: mark account as invalid
    await updateBankAccount(transaction.accountId, {
      status: 'invalid',
      disabledReason: returnCode === 'R03' ? 'No account' : 'Closed'
    });

    await updateTransaction(transaction.id, {
      status: 'failed',
      returnReason: `Account ${returnCode}`,
      requiresUserAction: true
    });

    // Notify user to add a new account
    await notifyUser(transaction.userId, {
      subject: 'Payment failed – add a new account',
      actionUrl: '/settings/bank-accounts/add'
    });

    return { action: 'fail_permanent', requiresNewAccount: true };
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling R10: Unauthorized Claim

R10 means the user claims they didn't authorize the transaction. This requires investigation.


javascript
async function handleAchReturn(returnCode, transaction) {
  if (returnCode === 'R10') {
    // Unauthorized claim: escalate to manual review
    await createFraudCase({
      transactionId: transaction.id,
      userId: transaction.userId,
      amount: transaction.amount,
      returnCode: 'R10',
      priority: 'high'
    });

    await updateTransaction(transaction.id, {
      status: 'under_review',
      returnReason:

---

*Decoding ACH return codes programmatically? The [ACH Return Codes API](https://rapidapi.com/payoutrail-ach-return-codes/api/ach-return-codes-api?utm_source=nichestream&utm_medium=devto&utm_campaign=payoutrail-ach-returns) returns the full Nacha R01–R85 set with plain-language descriptions and handling guidance.*
Enter fullscreen mode Exit fullscreen mode

Top comments (0)