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
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'
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.*
Top comments (0)