DEV Community

Payout Rail
Payout Rail

Posted on

ACH Return Codes Explained: R01, R03, R10 and How to Handle Them

ACH Return Codes Explained: R01, R03, R10 and How to Handle Them

When a payout fails, the reason matters. ACH return codes tell you exactly what went wrong—and whether your retry logic should fire immediately, wait, or pivot to a different payment rail entirely.

This guide covers the most common ACH return codes you'll encounter building payment integrations, what triggers them, and how to code a response.

The Big Three: R01, R03, R10

R01: Insufficient Funds

The account exists, routing is valid, but the balance is too low. This is the most frequent return (roughly 30–40% of all ACH returns in production systems).

When it fires: 1–2 business days after the debit entry is submitted.

How to handle it:

  • Flag the transaction as "insufficient funds" in your database.
  • Do not retry immediately; the customer's balance won't change in the next hour.
  • Implement exponential backoff: retry after 3 days, then 7 days.
  • Offer an alternative: email the customer with a link to retry, or suggest a lower payout amount.
async function handleACHReturn(returnCode, transaction) {
  if (returnCode === 'R01') {
    await db.updateTransaction(transaction.id, {
      status: 'failed_insufficient_funds',
      nextRetryAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days
      requiresCustomerAction: true
    });

    await sendCustomerEmail(transaction.userId, {
      subject: 'Your payout needs attention',
      body: 'Your account doesn't have enough funds. Try again in 3 days.'
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

R03: No Account / Unable to Locate Account

The routing number is valid, but the account number doesn't exist at that bank—or was closed.

When it fires: Usually within 24 hours.

How to handle it:

  • Do not retry. This is permanent.
  • Mark the bank account as invalid.
  • Prompt the user to re-enter their banking details.
  • If this is a recurring payout setup, pause all future payouts to this account.
if (returnCode === 'R03') {
  await db.updateBankAccount(account.id, { status: 'invalid' });
  await db.updateTransaction(transaction.id, { 
    status: 'failed_invalid_account',
    requiresNewBankDetails: true 
  });

  // Pause any scheduled payouts
  await db.pauseRecurringPayouts(account.userId);
}
Enter fullscreen mode Exit fullscreen mode

R10: Customer Advises Unauthorized

The customer contacted their bank and said they didn't authorize the debit. This is a dispute, not a technical failure.

When it fires: 10–180 days after the original entry (customers have up to 60 days to dispute).

How to handle it:

  • Log this as a chargeback or dispute.
  • Do not retry automatically.
  • Contact the customer directly—they may have disputed in error.
  • Preserve all evidence: timestamps, consent records, IP logs.
  • Consider flagging the customer's account for review if disputes are frequent.
if (returnCode === 'R10') {
  await db.createDispute({
    transactionId: transaction.id,
    type: 'unauthorized_claim',
    status: 'under_review',
    createdAt: new Date()
  });

  // Alert compliance/support
  await notifyComplianceTeam(transaction.userId, {
    reason: 'Customer dispute filed',
    transactionAmount: transaction.amount
  });
}
Enter fullscreen mode Exit fullscreen mode

Other Common Codes

Code Meaning Retry? Timeline
R02 Account closed No 24h
R04 Invalid routing No 24h
R05 Unauthorized user / account holder No 24h
R07 Authorization revoked No 1–2 days
R08 Payment stopped by customer No 1–2 days
R09 Uncollected funds Yes (after 5 days) 1–2 days
R16 Account frozen No 1–2 days
R20 Non-transaction account No 24h

Integration Pattern: Decode and Route


javascript
const returnCodeActions = {
  'R01': { retry: true, delay: 3 * 24 * 60 * 60, action: 'notify_customer' },
  'R03': { retry: false, action: 'invalidate_account' },
  'R10': { retry: false, action: 'create_dispute' },
  'R02': { retry

---

*Decoding ACH return codes programmatically? The [ACH Return Codes API](https://rapidapi.com/payoutrail-ach-return-codes/api/ach-return-codes-api?utm_source=nichestream&utm_medium=devto&utm_campaign=payoutrail-ach-returns) returns the full Nacha R01–R85 set with plain-language descriptions and handling guidance.*
Enter fullscreen mode Exit fullscreen mode

Top comments (0)