Building ACH Return Code Handling Into Your Payout System
When a payout fails, your system needs to know why—and what to do next. ACH return codes are the language your bank uses to tell you what went wrong. Understanding them isn't optional if you're moving money at scale.
What ACH Return Codes Actually Are
The National Automated Clearing House Association (Nacha) defines 85 standardized return codes (R01 through R85). Each one maps to a specific failure reason. Your payment processor receives the return, decodes it, and your system should act on it—either retry, escalate, or switch rails entirely.
Returns arrive in batches, typically 1–2 business days after the original debit entry. That delay is critical: your reconciliation logic must account for it.
The Most Common Return Codes
R01 — Insufficient Funds
The account doesn't have enough balance. This is the most frequent return you'll see. It's often temporary (account will have funds tomorrow) or permanent (customer is broke). Your retry logic should backoff and try again in 2–3 days, but cap retries at 3–4 attempts before flagging for manual review.
R03 — No Account / Invalid Account Number
The account number doesn't exist, or the routing number is wrong. This is permanent. No retry will fix it. Flag the recipient's bank details for verification and ask the customer to update their account information.
R10 — Customer Advises Not Authorized
The customer told their bank "I didn't authorize this." This is a dispute, not a technical failure. You need dunning logic here: contact the customer, understand why they disputed it, and decide whether to retry or refund.
R29 — Corporate Customer Advises Not Authorized
Same as R10, but for business accounts. Treat it identically.
R05 — Improper Debit Entry Classification
Your entry type doesn't match the account type (e.g., you sent a consumer debit to a business account). This is a configuration error. Fix your entry class code and retry.
R07 — Authorization Revoked by Customer
The customer revoked standing authorization. They don't want ACH debits from you anymore. Remove them from your ACH program and notify them.
R20 — Non-Transaction Account
The account exists but doesn't accept ACH debits (e.g., a savings account with restrictions). Verify the account type with the customer and ask them to provide a checking account instead.
Building Return Code Logic Into Your Code
Here's a minimal pattern:
def handle_ach_return(return_code, payout_id, recipient):
"""Decide next action based on Nacha return code."""
# Permanent failures: no retry
permanent_codes = {'R03', 'R04', 'R07', 'R20'}
# Temporary failures: retry with backoff
temporary_codes = {'R01', 'R09', 'R16'}
# Disputes: contact customer
dispute_codes = {'R10', 'R29'}
if return_code in permanent_codes:
mark_payout_failed(payout_id, 'permanent')
notify_customer(recipient, 'Update your bank details')
return 'manual_review'
elif return_code in temporary_codes:
retry_count = get_retry_count(payout_id)
if retry_count < 3:
schedule_retry(payout_id, days=2)
return 'scheduled_retry'
else:
mark_payout_failed(payout_id, 'max_retries')
return 'manual_review'
elif return_code in dispute_codes:
escalate_to_risk_team(payout_id, return_code)
return 'risk_review'
else:
# Unknown code: log and escalate
log_unknown_return(payout_id, return_code)
return 'manual_review'
Reconciliation and Timing
ACH returns don't arrive instantly. Build a reconciliation job that:
- Queries your processor's API for returns in the last 2 days
- Matches them to pending payouts by trace number
- Applies the logic above
- Updates payout status and triggers downstream workflows (refunds, notifications, etc.)
Run this daily, ideally in the morning before your retry window opens.
When to Switch Rails
If ACH is returning too often for a recipient, consider:
- RTP (Real-Time Payments): Settles in seconds, lower return rate for valid accounts
- Visa Direct: Higher cost, but reliable for consumer disbursements
ACH is cheap (~$0.25 per transaction) but slow. RTP costs more (~$0.50–$1.00) but fails faster if the account is bad. Have a fallback strategy.
Final Thought
Return codes are data. Treat them as signals for risk, not just errors. A spike in
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)