ACH Return Codes Explained: R01 to R85 and How to Handle Them
ACH Return Codes Explained: R01 to R85 and How to Handle Them
When an ACH transfer fails, the originating bank receives a standardized return code from NACHA (National Automated Clearing House Association). These codes tell you why a payout bounced—and they're critical for building reliable payment integrations.
Unlike generic "payment failed" messages, ACH return codes are deterministic. Each code maps to a specific reason and a specific recovery path. Understanding them means the difference between a retry loop that works and one that hemorrhages failed transactions.
The R-Code Universe
NACHA defines 85 return codes (R01–R85). Most fall into these categories:
- Insufficient funds (R01): Account has no money. Retry later or flag for manual review.
- Account closed (R02, R03): The account doesn't exist or is closed. Don't retry; update customer data.
- Invalid routing number (R03): Bank routing number is wrong. Validate against the FedACH directory.
- Unauthorized (R10): Originator (you) isn't authorized to debit this account. Check your authorization agreement.
- Duplicate (R15): Same payout already processed. Check your idempotency key.
- Account frozen (R16): Bank blocked the account. Contact customer.
Common Return Codes in Production
Here's a reference table for the codes you'll encounter most often:
| Code | Reason | Recoverable | Action |
|---|---|---|---|
| R01 | Insufficient funds | Yes | Retry in 2–5 days |
| R02 | Account closed | No | Update customer bank details |
| R03 | No account / invalid account | No | Validate routing + account number |
| R04 | Reserved (not used) | — | — |
| R10 | Unauthorized by originator | No | Review authorization; escalate |
| R15 | Duplicate entry | Yes | Deduplicate; retry if needed |
| R16 | Account frozen | No | Contact customer |
| R20 | Invalid company ID | No | Fix originator ID in batch |
| R23 | Entry type code invalid | No | Validate entry code in ACH file |
| R29 | Corporate record change | No | Update customer info |
Handling Returns Programmatically
When your payment processor returns an ACH return code, parse it and act:
async function handleACHReturn(returnCode, payoutId) {
const payout = await db.payouts.findById(payoutId);
// Non-recoverable codes: update customer and alert
const nonRecoverable = ['R02', 'R03', 'R10', 'R16', 'R20'];
if (nonRecoverable.includes(returnCode)) {
await db.payouts.update(payoutId, { status: 'failed', reason: returnCode });
await notifyCustomer(payout.customerId, `Bank details invalid: ${returnCode}`);
return;
}
// Recoverable codes: schedule retry
const recoverableWithDelay = {
'R01': 3 * 24 * 60 * 60 * 1000, // 3 days (insufficient funds)
'R15': 1 * 24 * 60 * 60 * 1000, // 1 day (duplicate)
};
if (recoverableWithDelay[returnCode]) {
const retryAt = new Date(Date.now() + recoverableWithDelay[returnCode]);
await db.payouts.update(payoutId, {
status: 'pending_retry',
nextRetryAt: retryAt,
returnCode: returnCode,
retryCount: (payout.retryCount || 0) + 1
});
// Cap retries at 3
if (payout.retryCount >= 3) {
await escalateToManualReview(payoutId);
}
return;
}
// Unknown code: log and escalate
logger.warn(`Unknown ACH return code: ${returnCode} for payout ${payoutId}`);
await escalateToManualReview(payoutId);
}
Key Patterns
Idempotency: Use a unique trace number or reference ID for each payout. If you see R15 (duplicate), your idempotency check failed—don't retry the same payout twice.
Timing: ACH returns arrive within 1–2 business days. Schedule retry logic to fire 2–3 days after the return, giving the customer time to fund their account.
Escalation: After 2–3 retries on a recoverable code, escalate to manual review rather than looping forever.
Alternate rails: On repeated R
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)