ACH Return Codes Explained: R01 to R85 and How to Handle Them in Production
Understanding ACH Return Codes in Your Payout System
When an ACH transaction fails, you don't get a generic "error" message. Instead, the National Automated Clearing House Association (Nacha) returns a specific two-character code that tells you exactly what went wrong. As a developer building payout infrastructure, learning these codes isn't optional—it's how you build reliable, debuggable systems that don't leave money stuck in limbo.
The ACH return code set spans R01 through R85, and each one signals a different problem: insufficient funds, closed accounts, authorization failures, or formatting errors. Understanding them means you can retry intelligently, alert customers accurately, and route transactions to fallback payment rails when needed.
The Most Common Return Codes You'll Encounter
R01: Insufficient Funds
The account doesn't have enough money to cover the debit. This is the most frequent return in production systems. Your handling strategy: flag the payout as retriable after a delay (typically 1–3 days), then retry once. If it fails a second time, notify the originator and consider an alternate funding source.
R03: No Account / Unable to Locate Account
The routing number and account number combination doesn't exist, or the account was closed. This is permanent and non-retriable. You should immediately mark the payout as failed, update your customer record to flag the account as invalid, and request updated banking details before attempting another transfer.
R04: Invalid Account Number Structure
The account number format is invalid—wrong length, invalid characters, or checksum failure. This is a data quality issue on your end. Validate account numbers at submission time using the mod-10 algorithm or your processor's validation API before batching.
R10: Customer Advises Unauthorized / Fraudulent
The receiver claimed they didn't authorize the transaction. This triggers a dispute and often a chargeback. Document the authorization proof and respond within the dispute window (typically 10 days). Consider implementing stronger verification for high-risk recipients.
R29: Corporate Account Closed
Similar to R03, but specific to business accounts. Non-retriable; request new banking details.
R31: Permissible Return Entry (CCD Entry Only)
The receiver's bank returned a CCD (Corporate Credit or Debit) entry because the originating company lacks authorization. Verify your CCD authorization agreements with the receiver's bank.
R37: Source Document Presented for Payment
The receiver disputes the transaction based on a source document (invoice, contract, etc.). This is a dispute flag; gather your documentation and respond within the dispute window.
Building Retry Logic Around Return Codes
Not all returns warrant a retry. Here's a decision tree:
| Return Code | Retriable? | Action |
|---|---|---|
| R01 | Yes (1–2 times) | Wait 1–3 days, then retry |
| R03, R04, R29 | No | Fail immediately, request new details |
| R10, R37 | No (but disputable) | Gather docs, respond to dispute |
| R16, R20, R21 | No | Fail; contact receiver's bank |
| R82 | Yes | Retry; likely a temporary processing error |
async function handleAchReturn(returnCode, payoutId) {
const payout = await getPayout(payoutId);
const nonRetriableCodes = ['R03', 'R04', 'R29', 'R10', 'R37'];
if (nonRetriableCodes.includes(returnCode)) {
await markPayoutFailed(payoutId, returnCode);
await notifyCustomer(payout.recipientId, 'permanent_failure', returnCode);
return;
}
const retriableCodes = ['R01', 'R82'];
if (retriableCodes.includes(returnCode)) {
const retryCount = await getRetryCount(payoutId);
if (retryCount < 2) {
await scheduleRetry(payoutId, 3); // retry in 3 days
await logEvent(payoutId, 'scheduled_retry', returnCode);
} else {
await markPayoutFailed(payoutId, 'max_retries_exceeded');
}
return;
}
// Unhandled code: escalate
await escalateToSupport(payoutId, returnCode);
}
Reconciliation and Timing
Returns don't arrive instantly. Standard ACH returns land 2–5 business days after the original debit date. Same-day ACH returns come back the same day. Your reconciliation logic must account for this window—don't assume a payout succeeded until the return window closes.
Next Steps
Integrate return code handling into your payout webhook handler. Log every return code with context (amount, recipient, date). Over time, patterns emerge: certain recipient banks return R01 more often, or specific account types trigger R03. Use that data to improve upstream validation and
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)