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.');
}
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.');
}
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);
}
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;
}
Building a Return Handler
In production, you'll want a centralized return processor that:
- Receives the return file from your ACH processor (usually daily)
- Decodes each return code
- Routes to the appropriate handler (retry, manual review, escalation)
- 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)