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 how to respond—is critical for building reliable fintech integrations. This guide maps the real Nacha return-code set to practical developer workflows.
Why ACH Returns Matter
ACH (Automated Clearing House) is the backbone of U.S. domestic payouts: low-cost, batch-based, and reversible. But reversibility cuts both ways. A transfer can be rejected or recalled up to five business days after initiation. Your code must detect, classify, and act on these returns without breaking the payout flow.
The Nacha Return Code Set (R01–R85)
Nacha defines 85 return codes. Here are the most common ones you'll encounter:
| Code | Reason | Recoverable? | Action |
|---|---|---|---|
| R01 | Insufficient funds | Yes | Retry after 2–3 days or route to alternate rail |
| R03 | No account / unable to locate | No | Mark recipient invalid; contact user |
| R04 | Invalid account number | No | Validate account format before next attempt |
| R05 | Account closed | No | Update recipient record; flag for manual review |
| R07 | Authorization revoked | No | Contact recipient; request new authorization |
| R08 | Payment stopped | Yes | Retry or escalate to user |
| R10 | Customer advises unauthorized | No | Investigate; may indicate fraud or dispute |
| R14 | Representative payee deceased | No | Verify recipient status |
| R16 | Account frozen | No | Contact recipient's bank |
| R20 | Non-transaction account | No | Confirm account type supports ACH |
| R29 | Corporate customer advises not authorized | No | Verify authorization chain |
How to Decode a Return in Code
When your ODFI (Originating Depository Financial Institution) or processor returns a file, parse the return code and route it:
async function handleAchReturn(returnRecord) {
const { transactionId, returnCode, amount, recipientId } = returnRecord;
const nonRecoverableCodes = ['R03', 'R04', 'R05', 'R07', 'R10', 'R14', 'R16', 'R20', 'R29'];
const recoverableCodes = ['R01', 'R08'];
if (nonRecoverableCodes.includes(returnCode)) {
// Log, alert ops, mark recipient as invalid
await db.recipients.update(recipientId, { status: 'invalid', returnCode });
await notifyUser(recipientId, `ACH failed: ${returnCode}. Contact support.`);
return { action: 'manual_review', retry: false };
}
if (recoverableCodes.includes(returnCode)) {
// Schedule retry after 2–3 business days
await scheduleRetry(transactionId, { delayDays: 3, maxAttempts: 2 });
return { action: 'retry_scheduled', nextAttempt: addDays(new Date(), 3) };
}
// Unknown code: escalate
await escalateToOps(transactionId, returnCode);
return { action: 'escalated' };
}
Timing and Reconciliation
ACH returns arrive in batches, typically:
- Same-day ACH returns: within hours (R03, R04, R05 often detected early)
- Next-day returns: standard ACH (R01, R08 may take 1–2 days)
- Delayed returns: up to 5 business days (rare, but possible)
Your reconciliation must account for this window. A transaction marked "sent" should not be marked "settled" until the return window closes (usually day 5). If you use same-day ACH, return windows are shorter but your settlement expectations change.
Building Retry Logic
Not all returns warrant a retry. Use this pattern:
const retryPolicy = {
R01: { retryable: true, delayDays: 3, maxAttempts: 2 }, // Insufficient funds
R08: { retryable: true, delayDays: 2, maxAttempts: 1 }, // Payment stopped
R03: { retryable: false }, // No account
R10: { retryable: false }, // Unauthorized
};
async function decideRetry(returnCode, attemptCount) {
const policy = retryPolicy[returnCode];
if (!policy) return false;
return policy.retryable && attemptCount < policy.maxAttempts;
}
When to Route to an Alternate Rail
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)