DEV Community

Payout Rail
Payout Rail

Posted on

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

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

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

When you're building a payout system, ACH returns are inevitable. A customer's bank rejects the transfer, and your code needs to know why—and what to do next. The National Automated Clearing House Association (Nacha) defines 85 return codes (R01 through R85). Understanding them isn't just compliance theater; it's the difference between a graceful retry and a broken payout loop.

Why ACH Returns Matter

ACH is slow by design. A typical ACH transfer settles in 1–2 business days, but returns can arrive 5–10 business days later. By then, you've already marked the payout as complete in your system. When a return lands, you need to:

  1. Detect the specific reason
  2. Decide whether to retry, escalate, or refund
  3. Update your ledger and notify the user
  4. Route to an alternate payment method if needed

The return code tells you which of these paths to take.

Common Return Codes and Developer Actions

R01: Insufficient Funds

What it means: The account doesn't have enough balance at settlement time.

When it fires: Usually 1–2 business days after initiation, when the ODFI (originating bank) attempts to debit the account.

How to handle:

if (returnCode === 'R01') {
  // Insufficient funds is often temporary
  // Retry after 3–5 business days
  scheduleRetry(payoutId, delayDays = 5);
  notifyUser('Payout delayed; insufficient funds. Will retry.');
}
Enter fullscreen mode Exit fullscreen mode

R03: No Account / Account Closed

What it means: The account number doesn't exist, or the account was closed before settlement.

When it fires: During ODFI validation, typically 1 business day after initiation.

How to handle:

if (returnCode === 'R03') {
  // This is permanent; don't retry
  markPayoutFailed(payoutId, reason = 'ACCOUNT_CLOSED');
  flagAccountForManualReview(recipientId);
  notifyUser('Account no longer exists. Please update banking details.');
}
Enter fullscreen mode Exit fullscreen mode

R10: Unauthorized

What it means: The recipient claims they didn't authorize this transfer (customer dispute or fraud).

When it fires: 5–10 business days after initiation (customer has time to dispute).

How to handle:

if (returnCode === 'R10') {
  // Escalate to compliance; don't auto-retry
  escalateToFraudTeam(payoutId);
  freezeAccount(recipientId);
  logEvent('UNAUTHORIZED_RETURN', {
    payoutId,
    amount,
    recipientBank,
    timestamp
  });
}
Enter fullscreen mode Exit fullscreen mode

R29: Corporate Account Closed

What it means: A business account was closed.

When it fires: 1–2 business days after initiation.

How to handle:

if (returnCode === 'R29') {
  markPayoutFailed(payoutId);
  notifyComplianceTeam('Corporate account closed; verify recipient status.');
}
Enter fullscreen mode Exit fullscreen mode

Return Code Categories

Nacha groups codes into broad buckets:

Category Codes Typical Action
Account Issues R03, R04, R29, R31 Fail; update account
Insufficient Funds R01, R09 Retry after delay
Invalid Account Data R05, R07, R08 Fail; validate routing/account
Authorization/Fraud R10, R11 Escalate; freeze account
Format/Technical R20, R21, R22 Investigate originator system
Timing Issues R16, R17, R18 Retry; may be transient

Building Retry Logic

Not all returns warrant a retry. A simple heuristic:

const RETRYABLE = new Set(['R01', 'R09', 'R16', 'R17', 'R18']);
const MAX_RETRIES = 3;

async function handleAchReturn(payoutId, returnCode) {
  const payout = await getPayout(payoutId);

  if (RETRYABLE.has(returnCode) && payout.retryCount < MAX_RETRIES) {
    scheduleRetry(payoutId, delayDays = 5);
  } else {
    markPayoutFailed(payoutId, returnCode);
    await routeToAlternateRail(payoutId); // Visa Direct, RTP, etc.
  }
}
Enter fullscreen mode Exit fullscreen mode

Settlement Reconciliation

ACH returns disrupt


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)