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

When an ACH transaction fails, your payout system receives a return code. Understanding what each code means—and how to respond programmatically—is critical to building reliable payment infrastructure.

What Are ACH Return Codes?

ACH return codes are three-character alphanumeric identifiers defined by Nacha (the National Automated Clearing House Association). When a debit or credit entry is rejected, the originating bank receives a return file within 1–2 business days containing one or more R-codes that explain why the transaction failed.

Your application must parse these codes, log them, and decide whether to retry, route to an alternate rail, or escalate to manual review.

Common ACH Return Codes and What They Mean

Code Name Cause Recoverable?
R01 Insufficient Funds Account balance too low Yes (retry later)
R02 Account Closed Account no longer active No (update records)
R03 No Account / Invalid Account Account number doesn't exist No (validate account)
R04 Invalid Account Number Structure Malformed routing or account No (validate format)
R05 Unauthorized User / Consumer Dispute Recipient disputes transaction No (contact recipient)
R07 Authorization Revoked Payer revoked permission No (request new auth)
R10 Customer Advises Not Authorized Payer claims no authorization No (investigate)
R16 Account Frozen Regulatory hold or fraud block No (contact bank)
R20 Non-Transaction Account Account not permitted for ACH No (use different account)
R29 Corporate Customer Advises Not Authorized Business account holder disputes No (investigate)
R31 Permissible Return Entry (CCD/CTX) Receiver returned entry per agreement Yes (contact receiver)

How to Handle Returns Programmatically

Step 1: Parse the Return File

ACH returns arrive as NACHA-formatted files (typically via SFTP or API). Parse the file to extract:

def parse_ach_return(return_file_content):
    """
    Parse ACH return file and extract return details.
    Returns list of dicts with payout_id, return_code, and reason.
    """
    returns = []
    lines = return_file_content.strip().split('\n')

    for line in lines:
        if line[0:1] == '6':  # Detail record
            return_code = line[20:23]  # Positions 20-22
            payout_id = line[79:87]    # Trace number (varies by format)

            returns.append({
                'payout_id': payout_id,
                'return_code': return_code,
                'timestamp': datetime.utcnow()
            })

    return returns
Enter fullscreen mode Exit fullscreen mode

Step 2: Classify the Return

Determine if the failure is recoverable:

RECOVERABLE_CODES = {'R01', 'R31'}
PERMANENT_CODES = {'R02', 'R03', 'R04', 'R05', 'R07', 'R10', 'R16', 'R20', 'R29'}

def classify_return(return_code):
    if return_code in RECOVERABLE_CODES:
        return 'retry'
    elif return_code in PERMANENT_CODES:
        return 'permanent_failure'
    else:
        return 'review'
Enter fullscreen mode Exit fullscreen mode

Step 3: Update Payout Status and Route Action


python
def handle_ach_return(payout_id, return_code):
    payout = db.query(Payout).filter_by(id=payout_id).first()
    classification = classify_return(return_code)

    if classification == 'retry':
        # Retry after 2–3 business days
        payout.status = 'pending_retry'
        payout.retry_count += 1
        payout.next_retry_at = datetime.utcnow() + timedelta(days=3)

        if payout.retry_count > 3:
            # Escalate after 3 failed attempts
            payout.status = 'escalated'
            notify_support(payout_id, return_code)

    elif classification == 'permanent_failure':
        payout.status = 'failed'
        payout.failure_reason = return_code
        notify_user(payout.user_id, f"Payout failed: {return_code}")

        # Optionally queue for alternate rail (RTP, Visa Direct)
        if should_retry_alternate_rail(payout):
            queue_for_rtp(payout)

---

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