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 push an ACH debit or credit into the banking system, you're not guaranteed it will land. The National Automated Clearing House Association (Nacha) defines 85 possible return codes—each one a specific reason why a payment failed. If you're building a payout platform, fintech app, or embedded payments feature, understanding these codes isn't optional; it's the difference between a smooth user experience and a broken reconciliation loop.

This article walks through the most common ACH return codes, what triggers them, and how to handle each one in your integration.

The Big Three: R01, R03, R10

R01: Insufficient Funds

The account exists and is valid, but the customer doesn't have enough money to cover the debit. This is the most common return code you'll see—typically 30–40% of all returns in a high-volume payout system.

Developer action: Retry after 3–5 business days. The customer may have deposited funds by then. If retries exhaust (usually after 2–3 attempts), flag the payout as failed and notify the customer. Don't immediately escalate to a support ticket; instead, queue it for a retry batch.

R03: No Account / Unable to Locate Account

The routing number is valid, but the account number doesn't exist or is closed. This is permanent—retrying won't help.

Developer action: Mark the payout as permanently failed. Initiate a customer notification flow asking them to verify their bank details. Consider offering an alternate payout method (card, wire, same-day ACH if available).

R10: Customer Advises Not Authorized

The customer claims they didn't authorize the debit. This is a dispute, not a technical failure.

Developer action: Treat this as a chargeback. Log it, notify compliance, and prepare documentation. Don't retry. Investigate whether the customer's authorization record is sound; if not, review your consent capture flow.

Timing-Related Returns: R14, R15, R16

R14: Representative Payee Deceased or Unable to Continue in That Capacity

Rare, but critical if you're processing government benefit payments.

R15: Beneficiary or Account Holder Deceased

Similar to R14. The account owner has passed away.

R16: Account Frozen

The bank froze the account due to legal hold, fraud investigation, or regulatory action.

Developer action for R14–R16: These are permanent. Flag the payout record, escalate to your compliance team, and halt future payouts to that account until manual review clears it.

Format & Validation Errors: R20, R21, R22

R20: Invalid Company Identification

Your company ID (your Originating Depository Financial Institution ID, or ODFI ID) doesn't match the originating bank's records.

R21: Invalid Account Number Structure

The account number format is invalid (e.g., contains letters, exceeds 17 digits, or fails a check-digit algorithm).

R22: Routing Number Check Digit Error

The routing number's check digit is wrong. This is a data-entry error on your side or the customer's.

Developer action for R20–R22: These indicate bad input data. Validate routing numbers and account numbers client-side before submission. Use the ABA routing number database or a validation library (e.g., Plaid, Dwolla, or Stripe's ACH validation). Catch these before they hit the bank.

Building Your Return-Code Handler

Here's a minimal pattern for handling returns in code:

RETURN_CODES = {
    "R01": {"category": "temporary", "retry_after_days": 3},
    "R03": {"category": "permanent", "retry_after_days": None},
    "R10": {"category": "dispute", "retry_after_days": None},
    "R14": {"category": "compliance", "retry_after_days": None},
    "R20": {"category": "validation", "retry_after_days": None},
}

def handle_ach_return(payout_id, return_code):
    rule = RETURN_CODES.get(return_code)
    if not rule:
        log_error(f"Unknown return code: {return_code}")
        return

    if rule["category"] == "temporary":
        schedule_retry(payout_id, days=rule["retry_after_days"])
    elif rule["category"] == "permanent":
        mark_failed(payout_id)
        notify_customer(payout_id, "Bank account issue—please update details")
    elif rule["category"] == "dispute":
        escalate_to_compliance(payout_id)
    elif rule["category"] == "validation":
        mark_failed(payout_id)
        flag_for_data_review(payout_id)
Enter fullscreen mode Exit fullscreen mode

Key


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)