Skip to content

How to test Nacha return codes: ACH return entries, time frames, and ledger effects

To test Nacha return codes, load synthetic ACH (Automated Clearing House) return files with addenda type 99 records and assert the payment state, ledger postings, and retry decision for each code. The check that catches the costliest defect sends an R10 unauthorized return on the last timely banking day, after the product has already released the funds.

Where ACH returns cost money after release

The defect is a product that treats a settled ACH debit as final. A payroll platform funds a pay run from an employer debit, an earned wage access product recovers an advance by debit, or a business account shows a pulled deposit as available. Two banking days later an R01 arrives, or eight weeks later an R10 arrives, and the money is already paid out.

The FFIEC (Federal Financial Institutions Examination Council) Retail Payment Systems booklet states that a properly authorized debit must be returned no later than the second banking day following settlement, that an ODFI (originating bank) normally charges a returned debit back to the originator, and that the ODFI may suffer a loss when the originator's account cannot cover it (checked on 2026-09-22). Its credit risk section adds that the return time frame for consumer debits that were not properly authorized extends under the Nacha rules to 60 days from the settlement date. When the fintech is the originator, the charged-back amount lands on its own account.

Returns also feed the Originator's return rates and the reinitiation rules, which our Nacha testing requirements page lists with the evidence an ODFI reviews.

What a return entry carries

A return is a new entry detail record followed by an addenda record of type 99. Field positions below come from the Nacha ISO 20022 Guide to Mapping U.S. ACH Return Items and Notifications of Change, version 2.01, August 2023:

FieldPositionsContent
Record Type Code01-01"7"
Addenda Type Code02-03"99"
Return Reason Code04-06R01, R02, and the other return codes
Original Entry Trace Number07-21Trace number of the forward entry
Date of Death22-27Only with R14 or R15
Original Receiving DFI Identification28-35Positions 04-11 of the forward entry
Addenda Information36-79Explanatory text from the RDFI, required with some codes such as R17
Trace Number80-94Trace number of the return entry detail record

In the return entry detail record, the same guide changes the transaction code to a return transaction code, the Receiving DFI Identification and check digit to the routing number of the original ODFI, and the trace number to the one assigned by the institution preparing the return. Every other field stays as in the original entry. Under the R27 (Trace Number Error) definition in that guide, an addenda trace number that differs from the trace number of the preceding entry detail record is an error.

Procedure

1. Generate the forward debit and its return

Build the return from the forward entry your product generated, so the fixture fails if the product changes a field the return copies. The excerpt below is a WEB debit of 125.00 USD, its return entry detail, and the addenda. Routing and account numbers are synthetic; use your test environment's values, and keep real account data out of fixtures.

Output
627234567898000123456789     0000012500SUB-0000417    SYNTHETIC RECEIVER    S 0123456780000417
626123456780000123456789     0000012500SUB-0000417    SYNTHETIC RECEIVER    S 1234567890000082
799R10123456780000417      23456789                                            234567890000082

Line 1 is the forward entry with transaction code 27 and trace 123456780000417. Line 2 is the return: transaction code 26, the ODFI routing and check digit in positions 04-12, addenda indicator 1, and the return trace. Line 3 carries R10, the original trace in 07-21, and the original RDFI (receiving bank) routing in 28-35.

2. Parse returns without crashing on bad input

Return files come from banks and BaaS partners, and a malformed line must produce a finding, never a stopped job.

Python
from datetime import date, timedelta

def parse_return_addenda(line: str) -> dict:
    """Parse a return addenda record (record type 7, addenda type 99). Never raises."""
    line = line.rstrip("\r\n")
    if len(line) != 94:
        return {"ok": False, "error": f"length {len(line)}, expected 94"}
    if line[0:1] != "7" or line[1:3] != "99":
        return {"ok": False, "error": f"not a return addenda: {line[0:3]!r}"}
    fields = {
        "return_reason_code": line[3:6],               # positions 04-06
        "original_entry_trace": line[6:21],            # positions 07-21
        "date_of_death": line[21:27].strip() or None,  # positions 22-27, R14 and R15 only
        "original_rdfi": line[27:35],                  # positions 28-35
        "addenda_information": line[35:79].rstrip(),   # positions 36-79
        "trace_number": line[79:94],                   # positions 80-94
    }
    code = fields["return_reason_code"]
    if not (code.startswith("R") and code[1:].isdigit()):
        return {"ok": False, "error": f"bad reason code {code!r}"}
    if not fields["original_entry_trace"].isdigit():
        return {"ok": False, "error": "original entry trace is not numeric"}
    if fields["date_of_death"] and code not in ("R14", "R15"):
        return {"ok": False, "error": "date of death on a code other than R14 or R15"}
    return {"ok": True, **fields}

Feed it a 90-character line, an addenda type 05 record, and a date of death on an R01. Each must return ok: False with a reason.

3. Assert the ledger effect per code

The schema is ours: ach_entries for forward entries, ach_returns for parsed returns, and ledger_postings keyed by trace number.

SQL
-- Check 1: each returned entry has exactly one reversal of the original amount
SELECT e.trace_number, e.amount_minor,
       COUNT(p.id) AS reversals, COALESCE(SUM(p.amount_minor), 0) AS reversed_minor
FROM ach_entries e
JOIN (SELECT DISTINCT original_trace FROM ach_returns) r ON r.original_trace = e.trace_number
LEFT JOIN ledger_postings p ON p.entry_trace = e.trace_number AND p.kind = 'ach_return_reversal'
GROUP BY e.trace_number, e.amount_minor
HAVING COUNT(p.id) <> 1 OR ABS(COALESCE(SUM(p.amount_minor), 0)) <> e.amount_minor;

-- Check 2: no new debit to the same account after R02, R03, R10, or R29
SELECT r.return_code, e.account_ref, n.trace_number AS later_debit
FROM ach_returns r
JOIN ach_entries e ON e.trace_number = r.original_trace
JOIN ach_entries n ON n.account_ref = e.account_ref
                  AND n.direction = 'debit'
                  AND n.created_at > r.received_at
WHERE r.return_code IN ('R02', 'R03', 'R10', 'R29');

Check 1 returns zero rows when every return reversed once. Send the same return twice: a second reversal is a duplicate posting, and the second return is what the Nacha guide calls R67, Duplicate Return. Check 2 encodes our test design, since an account the product has not re-validated should not receive another debit.

4. Time the R10 against the end of the window

The Nacha Reversals and Enforcement summary words the 60-day deadline, for a consumer return of an improper Reversal, as a return "made available to the ODFI by the opening of business on the banking day following the 60th calendar day following the settlement date" (checked on 2026-09-22). Differentiating Unauthorized Return Reasons gives R11 a 60-day time frame from 1 April 2020, and the FFIEC applies the 60-day window to consumer debits that were not properly authorized, which R10 covers. Our test applies the same end-of-window count to R10.

Python
def extended_window_end(settlement: date, banking_days: set[date]) -> date:
    """Banking day following the 60th calendar day after the original settlement date."""
    day = settlement + timedelta(days=61)
    while day not in banking_days:
        day += timedelta(days=1)
    return day

For a debit that settles on Tuesday 6 July 2027, the 60th calendar day is Saturday 4 September. Monday 6 September is Labor Day on the Federal Reserve holiday schedule, so the function returns Tuesday 7 September 2027, 63 calendar days after settlement. Inject the R10 in the first file of that Tuesday and assert that the reversal posts. Inject a second R10 on Wednesday 8 September and assert that the product flags it as a candidate for R68, Untimely Return, for the ODFI.

5. Replay dishonored and contested returns

The ODFI can dishonor a return with R61, R62, R67, R68, R69, or R70, and the RDFI can contest the dishonor with R71 to R77, including R73 Timely Original Return and R76 No Errors Found. The Nacha mapping guide states that dishonored returns must be sent in Nacha file format and are invalid in camt.053, and it leaves contested dishonored returns unmapped. The FedACH contested dishonored return form (version 1.1, 22 April 2024) chains three trace numbers: original, return, and dishonored return. Replay return, dishonor, and contest on one entry and store all three traces. In our matrix the expected ledger is reversal, re-posting, and a second reversal, with the final balance equal to a single return. The time limits for dishonor and contest are in the full Nacha Operating Rules, which Nacha sells; take them from your ODFI agreement.

The pitfalls outside vendor documentation

The window ends later than settlement plus 60. A product that releases a funds hold, closes a dispute reserve, or marks a debit final on the 60th calendar day loses the R10 that arrives in the 7 September file. The last timely day moves with weekends and Federal Reserve holidays, so compute it from a banking calendar, never as a fixed offset. R29 is different: the FFIEC describes the 60-day window for consumer debits, and Nacha keeps R29 for CCD and CTX (corporate) debits to non-consumer accounts, so our matrix tests R29 against the second-banking-day window.

camt.053 loses the Nacha code. In Table 1 of the Nacha mapping guide, R10, R20, and R29 all map to ISO code AG01, and R11 maps to NARR. A product that reads returns from a bank's camt.053 statement cannot count unauthorized returns from the ISO code alone. Confirm in the bank's implementation guide whether it carries the Nacha code in <AddtlInf>, and fail the test if the return rate job reads only <Rsn><Cd>. Statement parsing is covered in the camt.053 reconciliation guide.

Return rates count debits only. Nacha's calculation sheets, unauthorized and administrative or overall (January 2024), divide returned debits by debits originated over the preceding 60 days or two calendar months, or by debits in the files that held the original entries. The unauthorized rate counts R05, R07, R10, R11, R29, and R51 against 0.5%; the administrative level counts R02, R03, and R04 against 3.0%; the overall level counts all returned debits against 15.0%, with RCK entries allowed to be excluded from both sides of the overall calculation. A payroll originator sends mostly credits, and a report that divides by all entries hides a breach. Seed 1,000 synthetic debits, 9,000 credits, and 6 R10 returns: the correct unauthorized rate is 0.6%, while the all-entries figure of 0.06% shows no breach.

Test matrix

CodeWindow to testExpected payment stateExpected ledger effectNext debit to the account
R01 Insufficient FundsSecond banking day after settlementReturned, retry eligibleOne reversal of the original amountOnly as a reinitiated entry under Subsection 2.12.4, with "RETRY PYMT"
R02 Account ClosedSecond banking day after settlementReturned, account closedOne reversal of the original amountBlocked until the account is re-validated
R03 No Account or Unable to Locate AccountSecond banking day after settlementReturned, account invalidOne reversal of the original amountBlocked until the account is re-validated
R10 unauthorized consumer debitBanking day after the 60th calendar dayReturned, unauthorizedOne reversal, plus a receivable if funds were releasedBlocked; never reinitiated
R29 unauthorized corporate debitSecond banking day after settlementReturned, unauthorizedOne reversal of the original amountBlocked; never reinitiated
R67 after a duplicate returnAnyUnchanged after the second returnNo second reversalUnchanged
Dishonor then contest with R73Per ODFI agreementReturnedReversal, re-posting, reversalBlocked as for the original code

The same matrix runs against returned credits: an R03 on a payroll credit sends the funds back to the originator and must mark the employee's payment unpaid. We run this matrix as part of payment rails testing, and the ledger reconciliation guide shows the settlement-side checks.

Questions

What are ACH return codes?

ACH (Automated Clearing House) return codes are three-character codes that a receiving bank writes in positions 04-06 of the return addenda record to state why an ACH entry came back. R01 means insufficient funds, R02 account closed, R03 no account or unable to locate account, R10 an unauthorized consumer debit, and R29 an unauthorized corporate debit.

How long does an ACH return take?

An ACH (Automated Clearing House) debit that was properly authorized, such as one returned for insufficient funds or a closed account, must be returned no later than the second banking day after settlement. A return of an unauthorized debit to a consumer account can arrive up to 60 calendar days after the settlement date of the original debit.

What is an ACH return?

An ACH (Automated Clearing House) return is an entry that the receiving bank sends back through the ACH Network when it cannot or will not post the original entry. The return carries an addenda record with a return reason code and the original entry trace number, and the originating bank normally charges a returned debit back to the originator.

Scope your testing

Send FintechAssuranceLabs your product, segment, money flows, and the standards that need evidence, and get a proposed test scope with its deliverables.

Scope your testing