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 initiate an ACH transfer, you're not guaranteed success. The Nacha ruleset defines 85 possible return codes (R01 through R85) that tell you exactly why a payout failed. Understanding these codes isn't optional—it's critical to building reliable payment systems.

This article walks through the most common ACH return codes you'll encounter in production, what they mean, when they fire, and how your code should respond.

Why ACH Returns Matter

ACH is asynchronous. You submit a batch, it settles in 1–2 business days, and then you learn if it succeeded. Unlike card networks that decline in milliseconds, ACH returns arrive days later. That delay means your reconciliation, customer communication, and retry logic all depend on parsing these codes correctly.

The Most Common Return Codes

R01: Insufficient Funds

What it means: The account doesn't have enough balance to cover the debit.

When it fires: During settlement, usually 1–2 business days after submission.

How to handle it:

  • Log the return and flag the recipient's account.
  • Notify the customer that the transfer failed due to insufficient funds.
  • Retry after 2–3 business days (funds may be added), or route to an alternate payout method (Visa Direct, RTP).
{
  "return_code": "R01",
  "reason": "Insufficient Funds",
  "action": "retry_later",
  "retry_after_days": 3,
  "alternate_rail": "visa_direct"
}
Enter fullscreen mode Exit fullscreen mode

R03: No Account

What it means: The account number doesn't exist or is closed.

When it fires: During settlement validation.

How to handle it:

  • Do not retry. This is permanent.
  • Require the recipient to re-enter or verify their banking details.
  • Flag the recipient as needing account re-validation.
if (return_code === 'R03') {
  recipient.account_status = 'needs_verification';
  sendAccountVerificationEmail(recipient);
  return; // Don't retry
}
Enter fullscreen mode Exit fullscreen mode

R10: Unauthorized by Receiver

What it means: The account holder (or their bank) rejected the transfer, often due to fraud suspicion or revoked authorization.

When it fires: During settlement, sometimes days later if the recipient disputes it.

How to handle it:

  • Contact the recipient to confirm authorization.
  • Check if their bank has fraud filters enabled.
  • Consider routing to a different rail or asking for explicit re-authorization.

R29: Corporate Account Closed

What it means: The recipient's account is closed and cannot receive ACH transfers.

When it fires: During settlement.

How to handle it:

  • Similar to R03: don't retry.
  • Ask the recipient to provide an active account.
  • Flag in your system to prevent future attempts to this account.

R31: Permissible Return by Receiver

What it means: The recipient's bank returned the transfer per the customer's request (not fraud, just "I don't want this").

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

How to handle it:

  • Notify the recipient that they rejected the transfer.
  • Ask if they want to retry or use a different account.
  • Respect their choice; don't auto-retry.

Building Return Code Logic

Here's a pattern for handling returns programmatically:

const ACH_RETURN_HANDLERS = {
  R01: { retryable: true, delay_days: 3, message: 'Insufficient funds' },
  R03: { retryable: false, delay_days: 0, message: 'Account does not exist' },
  R10: { retryable: true, delay_days: 5, message: 'Unauthorized by receiver' },
  R29: { retryable: false, delay_days: 0, message: 'Account closed' },
  R31: { retryable: false, delay_days: 0, message: 'Rejected by receiver' },
};

function handleACHReturn(return_code, payout_id) {
  const handler = ACH_RETURN_HANDLERS[return_code];

  if (!handler) {
    console.error(`Unknown return code: ${return_code}`);
    return;
  }

  if (handler.retryable) {
    scheduleRetry(payout_id, handler.delay_days);
  } else {
    markPayoutFailed(payout_id, handler.message);
    notifyRecipient(payout_id, handler.message);
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • R01, R10: Retryable; schedule a retry after 2–5 days.
  • R03, R29, R31: Not ret

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)