Payout RailACH Return Codes Explained: R01–R85 and How to Handle Them in Production ACH Return Codes...
ACH Return Codes Explained: R01–R85 and How to Handle Them in Production
When you're building a payout system, ACH returns are inevitable. A user's bank rejects the transfer, your API gets a return code, and you need to decide: retry, notify the user, or switch rails entirely. But what does each code actually mean, and when should it fire?
This guide walks through the most common ACH return codes (defined by Nacha, the clearing house rules body) and shows you how to decode them in production code.
Every ACH return follows the same pattern:
The code tells you why the bank rejected the entry, which determines whether you should retry, escalate to the user, or fail permanently.
When it fires: The account doesn't have enough balance to cover the debit.
What to do: This is temporary. Retry after a few days, or prompt the user to deposit funds. Don't retry immediately—the account won't suddenly be funded.
When it fires: The account number doesn't exist, or the account has been closed.
What to do: Permanent failure. Ask the user to verify their routing and account number. Don't retry.
When it fires: The account number format is invalid (e.g., too short, non-numeric).
What to do: Validation error. This should rarely reach ACH—catch it client-side. If it does, ask the user to re-enter.
When it fires: The account holder disputes the transaction (fraud claim, duplicate, or genuinely unauthorized).
What to do: Permanent failure. Contact the user immediately. This can escalate to a chargeback if not resolved.
When it fires: Your originating company ID (SEC code) is wrong or unregistered.
What to do: This is your problem, not the user's. Check your Nacha registration and retry with the correct ID.
When it fires: A business account holder says the debit wasn't authorized.
What to do: Same as R10—escalate to your compliance team. This is a dispute.
When it fires: The routing number's check digit doesn't match.
What to do: Validation error. Verify the routing number and re-submit.
Here's a pattern for decoding returns and deciding next steps:
const handleAchReturn = (returnCode, metadata) => {
const permanentCodes = ['R03', 'R04', 'R10', 'R29'];
const retryableCodes = ['R01', 'R08'];
const validationCodes = ['R20', 'R33'];
if (permanentCodes.includes(returnCode)) {
// Mark payout as failed; notify user
return {
status: 'failed',
action: 'contact_user',
message: `Bank rejected transfer (${returnCode}). Please verify account details.`
};
}
if (retryableCodes.includes(returnCode)) {
// Schedule retry in 3–5 business days
return {
status: 'pending_retry',
action: 'schedule_retry',
retryAfter: addBusinessDays(new Date(), 3)
};
}
if (validationCodes.includes(returnCode)) {
// Your error; fix and re-submit
return {
status: 'pending_resubmit',
action: 'contact_support',
message: `ACH validation error (${returnCode}). Engineering team alerted.`
};
}
// Unknown code—escalate
return {
status: 'unknown',
action: 'escalate',
message: `Unexpected return code: ${returnCode}`
};
};
ACH returns arrive 1–5 business days after the original entry is submitted. Your reconciliation logic must account for this delay:
Always query your ACH provider's return status endpoint before marking a payout as settled.
Not all returns are created equal. R01 (insufficient funds) is temporary;
Decoding ACH return codes programmatically? The ACH Return Codes API returns the full Nacha R01–R85 set with plain-language descriptions and handling guidance.