DEV Community

Payout Rail
Payout Rail

Posted on

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

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

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

When an ACH transaction fails, the originating bank receives a return code. Understanding what each code means—and how to respond—is critical for any developer building payout systems, payroll integrations, or marketplace payment flows.

Unlike credit card declines, which happen in milliseconds, ACH returns arrive 1–5 business days after the transaction posts. By then, your system may have already marked the payout as complete. A well-designed integration catches these codes, logs them, and decides whether to retry, escalate, or route to an alternate payment rail.

The Most Common ACH Return Codes

The National Automated Clearing House Association (Nacha) maintains the official R-code set. Here are the ones you'll encounter most often:

Code Meaning Cause Retry?
R01 Insufficient funds Account balance too low Yes (after funds available)
R03 No account / unable to locate Account closed or number wrong No
R04 Invalid account number Malformed or fake account No
R05 Account closed by institution Bank closed the account No
R07 Authorization revoked Receiver revoked permission No
R10 Customer advises not authorized Receiver claims they didn't authorize No
R29 Corporate customer advises not authorized Business receiver disputes No
R31 Permissible return by receiver Receiver requested return (soft return) Maybe
R51 Receiver does not participate in ACH Rare; account not ACH-eligible No
R82 Non-transaction account Savings account, not checking No

Return codes R01–R85 cover everything from technical failures (R02: route not found) to fraud disputes (R10, R29) to settlement issues (R82: wrong account type).

Handling R01: Insufficient Funds

R01 is the most common return and the most likely to succeed on retry.

async function handleAchReturn(returnCode, payoutId, recipientAccount) {
  if (returnCode === 'R01') {
    // Insufficient funds — likely temporary
    const payout = await db.getPayoutById(payoutId);

    // Schedule retry in 3 business days
    await db.updatePayout(payoutId, {
      status: 'pending_retry',
      nextRetryDate: addBusinessDays(new Date(), 3),
      returnCode: 'R01',
      attemptCount: payout.attemptCount + 1
    });

    // Log for monitoring
    logger.info(`R01 return for payout ${payoutId}; retry scheduled`);

    // Optionally notify recipient
    await notificationService.sendEmail(payout.recipientEmail, 
      'Your payout is pending. We'll retry in 3 business days.');

    return { action: 'retry_scheduled' };
  }
}
Enter fullscreen mode Exit fullscreen mode

Retry R01 up to 3 times, spaced 3–5 business days apart. After that, escalate to manual review.

Handling R03, R04, R05: Account Issues

These codes indicate the account itself is invalid or closed. Do not retry.

if (['R03', 'R04', 'R05'].includes(returnCode)) {
  // Account is bad; need new banking details
  await db.updatePayout(payoutId, {
    status: 'failed_account_invalid',
    returnCode: returnCode
  });

  // Notify recipient to update account
  await notificationService.sendSMS(payout.recipientPhone,
    `Your payout failed (${returnCode}). Please update your bank details.`);

  // Flag for manual review if high-value payout
  if (payout.amount > 10000) {
    await escalationQueue.add({
      payoutId,
      reason: 'Account invalid; manual review needed'
    });
  }

  return { action: 'failed_account_invalid', requiresManualReview: true };
}
Enter fullscreen mode Exit fullscreen mode

Handling R10, R29: Unauthorized Claims

R10 (customer disputes authorization) and R29 (corporate customer disputes) are fraud-adjacent. The receiver is claiming they never approved the payout.


javascript
if (['R10', 'R29'].includes(returnCode)) {
  // Potential fraud or permission issue
  await db.updatePayout(payoutId, {
    status: 'disputed',
    returnCode: returnCode,
    flaggedForFraudReview: true
  });

  // Notify compliance team
  await complianceQueue.add({

---

*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)