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 transaction fails, the National Automated Clearing House Association (Nacha) returns it with a specific code. Understanding these codes—and building logic around them—is critical for any developer managing payouts, payroll, or B2B transfers at scale.
Unlike a generic "payment failed" error, an ACH return code tells you why the transaction failed and whether retrying makes sense. Mishandling a return can leave your users stranded and your reconciliation a mess.
The Core Return Codes You'll Encounter
The Nacha ruleset defines 85 return codes (R01–R85). Here are the ones that account for ~85% of real-world failures:
| Code | Reason | Permanent? | Retry? |
|---|---|---|---|
| R01 | Insufficient funds | No | Yes, after 1–3 days |
| R02 | Account closed | Yes | No |
| R03 | No account / unable to locate | Yes | No |
| R04 | Invalid account number | Yes | No |
| R05 | Account frozen / blocked | Maybe | No (contact required) |
| R07 | Authorization revoked | Yes | No |
| R10 | Customer advises unauthorized | Yes | No |
| R14 | Representative payee deceased | Yes | No |
| R29 | Corporate account restricted | Yes | No |
| R31 | Permissible use violation | Yes | No |
R01 (Insufficient Funds) is the most common and the most actionable. The account exists and is valid; there's simply not enough money at settlement time. Retrying 2–3 business days later often succeeds.
R02, R03, R04 are permanent failures—the account is closed, doesn't exist, or the number is wrong. No retry helps; you need to contact the recipient and correct the account data.
R10 (Unauthorized) means the customer claims they didn't authorize the payment. This is a dispute flag and typically a sign to halt further payouts to that account until resolved.
Building a Return Handler
Here's a concrete pattern for decoding and acting on returns:
async function handleAchReturn(returnCode, transactionId, amount, recipientId) {
const permanentFailureCodes = ['R02', 'R03', 'R04', 'R07', 'R10', 'R14', 'R29', 'R31'];
const retryableCodes = ['R01'];
if (permanentFailureCodes.includes(returnCode)) {
// Mark account as invalid; notify user
await db.recipients.update(recipientId, { status: 'invalid', returnCode });
await notifyUser(recipientId, `Payment failed (${returnCode}). Please verify account details.`);
return { action: 'halt', retry: false };
}
if (retryableCodes.includes(returnCode)) {
// Schedule retry in 3 days
await db.payouts.update(transactionId, {
status: 'pending_retry',
nextRetry: addDays(new Date(), 3),
returnCode
});
await notifyUser(recipientId, `Payment pending retry. We'll try again in 3 days.`);
return { action: 'retry', nextRetry: addDays(new Date(), 3) };
}
// Unknown or edge-case code
await logAlert('unknown_return_code', { returnCode, transactionId });
return { action: 'manual_review' };
}
Timing Matters
ACH returns arrive 2–5 business days after the original debit date (not settlement). Your reconciliation logic must account for this lag. If you're batching payouts daily, you won't know about returns for days—so your dashboard and ledger need to mark transactions as "pending return window" until the window closes.
When to Route Around ACH
If a recipient has a history of R01 returns (insufficient funds) but the amount is small and time-sensitive, consider offering RTP (Real-Time Payments) or Visa Direct as a fallback. RTP settles in seconds; Visa Direct in minutes. Both cost more (~$0.25–$1.00 per transaction vs. $0.10–$0.25 for ACH), but they eliminate the return-and-retry cycle.
if (returnCode === 'R01' && amount < 5000 && isTimesSensitive) {
// Offer Visa Direct as alternative
await suggestAlternateRail(recipientId, 'visa_direct', amount);
}
Key Takeaways
- **Decode every return code.
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)