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

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)