DEV Community

Payout Rail
Payout Rail

Posted on

ACH Return Codes Explained: R01 to R85 and How to Handle Them

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' };
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)