DEV Community

Payout Rail
Payout Rail

Posted on

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

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

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

When you're building a payout system, ACH returns are inevitable. A transaction that looked good when you submitted it can come back rejected days later with a cryptic two-character code. Understanding what those codes mean—and how to respond—is the difference between a robust payout platform and one that silently loses money or frustrates users.

The National Automated Clearing House Association (NACHA) defines 85 return codes (R01 through R85). Each one tells you something specific about why a bank rejected an ACH entry. Let's walk through the most common ones you'll encounter in production and how to handle them programmatically.

The Big Three: R01, R03, R10

R01: Insufficient Funds

This is the most common return code you'll see. The account exists, the routing number is valid, but there isn't enough money to cover the debit. In production, this typically means:

  • The recipient's account balance dropped between submission and settlement
  • A competing debit hit the account first
  • The amount was larger than expected

How to handle it: Log the return, notify the recipient, and offer to retry in 1–3 days. Don't immediately mark the payout as failed. Many R01s resolve on retry because the account is replenished.

if (returnCode === 'R01') {
  await updatePayoutStatus(payoutId, 'RETURNED_INSUFFICIENT_FUNDS');
  await scheduleRetry(payoutId, { delayDays: 2, maxRetries: 3 });
  await notifyRecipient(recipientId, 'Payout failed due to insufficient funds. We'll retry in 2 days.');
}
Enter fullscreen mode Exit fullscreen mode

R03: No Account / Unable to Locate Account

The routing number is valid, but the account number doesn't exist at that bank. This is a permanent failure—retrying won't help.

How to handle it: Flag this for manual review. Ask the user to verify their account number and routing number. This is a data-entry problem, not a transient one.

if (returnCode === 'R03') {
  await updatePayoutStatus(payoutId, 'RETURNED_INVALID_ACCOUNT');
  await flagForManualReview(payoutId, 'Account does not exist. Verify account details.');
  await notifyRecipient(recipientId, 'Please verify your bank account number and routing number.');
}
Enter fullscreen mode Exit fullscreen mode

R10: Customer Advises Not Authorized

The recipient's bank received the debit but the account holder says they didn't authorize it. This is a dispute, and it signals a serious problem: either the recipient's account was compromised, or they're claiming fraud to reverse a legitimate payout.

How to handle it: Don't retry. Lock the recipient's account pending investigation. Contact them directly and escalate to compliance.

if (returnCode === 'R10') {
  await updatePayoutStatus(payoutId, 'RETURNED_UNAUTHORIZED');
  await suspendRecipient(recipientId, 'Payout flagged as unauthorized. Account locked pending review.');
  await escalateToCompliance(payoutId, recipientId);
}
Enter fullscreen mode Exit fullscreen mode

Medium-Frequency Returns: R04, R07, R29

Code Meaning Permanent? Typical Action
R04 Reserved (not currently used by NACHA) N/A Log and escalate
R07 Authorization revoked by customer Yes Contact recipient; may indicate fraud
R29 Corporate customer advises not authorized Yes Escalate to compliance immediately

Rare but Critical: R82, R83

R82: Routing Number Check Digit Error

The routing number format is invalid. This should never happen if you validate on input, but if it does, the payout was rejected before it ever reached the recipient's bank.

R83: Invalid Account Number Check Digit

Similar to R82, but for the account number. Again, validate on input to prevent submission.

function validateACHAccount(routingNumber, accountNumber) {
  // Validate routing number format (9 digits, valid check digit)
  if (!isValidRoutingNumber(routingNumber)) {
    throw new Error('Invalid routing number check digit');
  }
  // Validate account number (typically 1–17 digits, no special chars)
  if (!/^\d{1,17}$/.test(accountNumber)) {
    throw new Error('Invalid account number format');
  }
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Building a Return Handler

In production, you'll want a centralized return processor that:

  1. Receives the return file from your ACH processor (usually daily)
  2. Decodes each return code
  3. Routes to the appropriate handler (retry, manual review, escalation)
  4. Updates the p

Decoding ACH return codes programmatically? The ACH Return Codes API returns the full Nacha R01–R85 set with plain-language descriptions and handling guidance.

Top comments (0)