Regulation E Error Resolution for ACH Disputes
When a consumer notifies you that an ACH debit was unauthorized, duplicated, or posted in the wrong amount, Regulation E stops being a policy document and becomes a stopwatch. Under 12 CFR 1005.11 you have ten business days to investigate, and if you cannot finish in that window you must issue provisional credit to keep investigating out to forty-five days — or ninety for a defined set of transactions — followed by a results notice within three business days of conclusion. Within the broader Reg E, NACHA and audit controls framework, this reference specifies those obligations at the subsection level and shows how to implement them as guarded, attributable state rather than a spreadsheet of due dates. A dispute is itself a reconciliation exception, so it flows through the same exception lifecycle state machine as any other break, with the Reg E clock attached as metadata.
The engineering hazard is that the clock rarely runs on the calendar an untrained implementation assumes. The ten-, forty-five-, and ninety-day counts are business days, they exclude Federal Reserve holidays, and they start from the date of notice rather than the date of the transaction — while provisional credit, once the ten-day mark passes without resolution, becomes a mandatory action whose own reversal has a deadline. That provisional-credit obligation is a guarded transition in its own right; the state-machine mechanics of it are worked through in encoding provisional-credit state transitions, and the two dedicated references below make the timeline and the credit decision copy-paste concrete.
The scope of §1005.11 is specific: it governs consumer accounts and electronic fund transfers. A commercial ACH dispute or a wire falls under UCC Article 4A and its own contractual timelines, not Reg E — misclassifying a commercial account as consumer is one of the four failure modes catalogued below, and it starts a clock the statute never required.
The Statutory Timeline at Subsection Level
12 CFR 1005.11 lays out the machinery in a fixed order, and each step has a citable subsection. A notice of error (§1005.11(b)) is timely only if the institution receives it within sixty days of transmitting the periodic statement showing the alleged error; a notice outside that window does not trigger the mandatory error-resolution procedures. Once a timely notice arrives, §1005.11(c)(1) sets the baseline: investigate and determine whether an error occurred within ten business days of receiving the notice, then correct any error within one business day of determining it occurred and report the results within three business days of finishing the investigation.
The extension is conditional, not automatic. Section 1005.11(c)(2) permits taking up to forty-five calendar days to investigate — but only if the institution provisionally credits the disputed amount (plus, where applicable, any related fees and interest) within ten business days of the notice and tells the consumer of the credit within two business days of providing it. For new accounts (opened within thirty days before the disputed transaction), point-of-sale debit-card transactions, and foreign-initiated transfers, §1005.11(c)(3) extends those figures to twenty business days for the base investigation and ninety calendar days for the provisional-credit-backed extension. The results notice under §1005.11(d) is due within three business days of concluding the investigation, and if no error (or a different error) is found, §1005.11(d)(2) requires notice of the reversal and continued honoring of items for five business days.
Phase 1 — Model the Dispute as Guarded State
A dispute record must carry everything the clocks depend on: the notice date, the account classification, the transaction class, the disputed amount in cents, and whether provisional credit has been issued. Model it as a typed record whose fields the deadline calculator reads, never as loose columns an analyst edits by hand.
from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from enum import Enum
class AccountType(Enum):
CONSUMER = "consumer"
COMMERCIAL = "commercial" # NOT covered by Reg E — routes to UCC 4A
class TxnClass(Enum):
STANDARD = "standard"
NEW_ACCOUNT = "new_account" # opened <=30 days before the txn
POINT_OF_SALE = "pos"
FOREIGN_INITIATED = "foreign"
@dataclass(frozen=True, slots=True)
class Dispute:
dispute_id: str
txn_id: str
account_type: AccountType
txn_class: TxnClass
amount_cents: int
notice_date: date # date the institution RECEIVED notice
statement_date: date # periodic statement showing the error
provisional_credit_issued: bool = False
def is_reg_e(self) -> bool:
"""Reg E error resolution applies only to consumer EFTs."""
return self.account_type is AccountType.CONSUMER
def within_60_day_bar(self) -> bool:
"""§1005.11(b): notice must arrive within 60 days of the statement."""
return (self.notice_date - self.statement_date).days <= 60
Freezing the record matters more than it looks. A dispute's classification is the input every deadline depends on, so an analyst who quietly flips txn_class from STANDARD to NEW_ACCOUNT after credit has issued has silently moved four deadlines and invalidated the audit trail that justified them. Reclassification should be a new, attributed event that supersedes the prior record, never an in-place mutation — which is exactly why Dispute is frozen=True. The intake step that constructs it is also where the two derived predicates belong: is_reg_e() decides whether the statute applies at all, and within_60_day_bar() decides whether the mandatory procedures are even triggered. Both should be evaluated and logged the moment the dispute is created, not recomputed ad hoc downstream.
Phase 2 — Compute the Deadlines on a Banking-Day Calendar
The calculator converts the classification into concrete dates. Business-day counts skip weekends and Federal Reserve holidays; calendar-day counts (the 45/90 extension caps) do not. The full annotated banking-day implementation lives in encoding Reg E dispute timelines in Python; the deadline shape it returns is below.
from __future__ import annotations
from dataclasses import dataclass
from datetime import date, timedelta
from typing import Callable
@dataclass(frozen=True, slots=True)
class RegEDeadlines:
investigation_due: date # 10 (or 20) business days from notice
provisional_credit_due: date # same as investigation_due if unresolved
extended_resolution_due: date # 45 or 90 calendar days from notice
results_notice_due: date # 3 business days after conclusion
def compute_deadlines(
dispute: Dispute,
add_business_days: Callable[[date, int], date],
) -> RegEDeadlines:
"""Derive every Reg E deadline from the notice date and classification.
`add_business_days` must honor weekends and Fed holidays; passing naive
calendar arithmetic here is the most common cause of a blown deadline.
"""
special = dispute.txn_class in (
TxnClass.NEW_ACCOUNT, TxnClass.POINT_OF_SALE, TxnClass.FOREIGN_INITIATED,
)
base_business_days = 20 if special else 10
extension_calendar_days = 90 if special else 45
investigation_due = add_business_days(dispute.notice_date, base_business_days)
return RegEDeadlines(
investigation_due=investigation_due,
provisional_credit_due=investigation_due,
extended_resolution_due=dispute.notice_date + timedelta(days=extension_calendar_days),
results_notice_due=add_business_days(investigation_due, 3),
)
Note that provisional_credit_due deliberately coincides with investigation_due: the credit is what buys the extension, so it must be on the ledger by the same date the base investigation would otherwise have had to conclude. The extended_resolution_due is a calendar-day figure and therefore ignores the banking-day helper entirely — passing forty-five or ninety through add_business_days would stretch the window by roughly a third and hand you a deadline the statute never granted. Keeping the two arithmetics in visibly separate expressions is not stylistic; it is the guardrail that stops a maintainer from "consistency-fixing" the calendar cap into a business-day count. The results_notice_due chains off investigation_due because the three-day notice window runs from the conclusion of the investigation, which in the unresolved-and-extended case is bounded by the extension cap, not the base window.
Phase 3 — Gate Provisional Credit and the Results Notice
Provisional credit becomes mandatory the instant the ten-business-day investigation lapses without a determination. Whether to issue it — and the amount and reversal deadline — is a decision with its own guardrails, developed fully in automating provisional-credit decisions under Reg E. The gate below is the minimal rule: if today is past the investigation due date and the dispute is unresolved, credit is owed; issuing it unlocks the extended clock, and every transition writes to the append-only audit trail.
def provisional_credit_required(
dispute: Dispute, deadlines: RegEDeadlines, today: date, resolved: bool,
) -> bool:
"""Credit is mandatory once the investigation window lapses unresolved."""
if not dispute.is_reg_e() or resolved:
return False
return today > deadlines.investigation_due and not dispute.provisional_credit_issued
Failure Modes & Guardrails
| Failure scenario | Root cause | Mitigation |
|---|---|---|
| Deadline lands on a weekend or Fed holiday | Business-day count implemented as notice_date + timedelta(days=10) |
Use a banking-day calendar that skips Saturdays, Sundays, and the ten Federal Reserve holidays; never mix calendar and business-day arithmetic |
| Provisional credit never issued | Investigation silently ran past ten business days with no gate | Fire provisional_credit_required on every tick past investigation_due; block auto-close of an unresolved dispute past that date |
| Wrong clock for a new account | txn_class defaulted to STANDARD when the account was opened within 30 days |
Derive NEW_ACCOUNT from account-open date at dispute intake, not from analyst entry; apply 20 business-day / 90-day figures |
| 60-day bar miscalculated | Bar measured from transaction date instead of statement date | Measure §1005.11(b) from the periodic-statement date that showed the error; a late notice does not trigger mandatory procedures but must still be logged |
| Commercial dispute run under Reg E | Account type misclassified as consumer | Gate on is_reg_e(); route commercial disputes to the UCC 4A path with its contractual timelines, not the §1005.11 clocks |
Compliance & Auditability
Every deadline and every credit is a regulated control surface, so cite the subsection in the audit record, not just the date. §1005.11(b) — the timeliness of the notice (60 days from statement) — determines whether the mandatory procedures apply at all; log the statement date and notice date so the bar is reconstructable. §1005.11(c)(1) governs the ten-business-day base investigation and the one-business-day correction; §1005.11(c)(2) the 45-calendar-day extension conditioned on provisional credit within ten business days; §1005.11(c)(3) the 20-business-day / 90-calendar-day figures for new accounts, POS, and foreign-initiated transfers. §1005.11(d) covers the three-business-day results notice and, on a no-error finding, the reversal notice and five-business-day honor period. Each state transition — dispute opened, credit issued, investigation concluded, results sent, credit reversed — must append an event carrying the actor, timestamp, and governing subsection so an examiner can replay the full lifecycle against the calendar.
The subsection tag is what makes an audit defensible rather than merely detailed. A due date on its own tells an examiner when you acted; the citation tells them which obligation you were discharging, and the two together let them verify the date was computed under the right rule. This matters most in the extension case: a 45-day resolution that cites §1005.11(c)(2) implicitly asserts that provisional credit was issued by the ten-business-day mark, because the extension is not available otherwise. If the audit trail shows the extended deadline being relied upon but no credit-issued event before investigation_due, that is a self-evident compliance gap an examiner will spot immediately — which is precisely the kind of contradiction an append-only, subsection-tagged log surfaces automatically instead of hiding. Retain these records for the Reg E minimum of two years, and longer where a legal hold or a broader SOX retention policy applies.
Testing & Verification
Deadline logic is exactly the kind of code that passes a naive unit test and fails in production on a holiday week. Test against known-hard dates: a Friday notice, a notice the week of a Federal Reserve holiday, and the new-account boundary.
import pytest
from datetime import date
def test_new_account_uses_20_business_days(add_business_days):
dispute = Dispute(
dispute_id="D1", txn_id="T1",
account_type=AccountType.CONSUMER, txn_class=TxnClass.NEW_ACCOUNT,
amount_cents=25_00, notice_date=date(2026, 3, 2),
statement_date=date(2026, 2, 20),
)
d = compute_deadlines(dispute, add_business_days)
# 20 business days, not 10, for a new account
assert d.investigation_due == add_business_days(date(2026, 3, 2), 20)
# 90 calendar days for the extended resolution
assert d.extended_resolution_due == date(2026, 3, 2) + __import__("datetime").timedelta(days=90)
def test_provisional_credit_required_after_investigation_lapses(add_business_days):
dispute = Dispute(
dispute_id="D2", txn_id="T2",
account_type=AccountType.CONSUMER, txn_class=TxnClass.STANDARD,
amount_cents=140_00, notice_date=date(2026, 3, 2),
statement_date=date(2026, 2, 25),
)
d = compute_deadlines(dispute, add_business_days)
after = d.investigation_due + __import__("datetime").timedelta(days=1)
assert provisional_credit_required(dispute, d, today=after, resolved=False)
def test_commercial_account_is_not_reg_e(add_business_days):
dispute = Dispute(
dispute_id="D3", txn_id="T3",
account_type=AccountType.COMMERCIAL, txn_class=TxnClass.STANDARD,
amount_cents=500_00, notice_date=date(2026, 3, 2),
statement_date=date(2026, 2, 25),
)
assert not dispute.is_reg_e()
Frequently Asked Questions
When does the Reg E clock actually start?
On the date the institution receives the consumer's notice of error, not the transaction date and not the statement date. Section 1005.11(c) counts the ten (or twenty) business days from that receipt. The statement date matters only for the separate §1005.11(b) timeliness bar — a notice must arrive within sixty days of the statement showing the error for the mandatory procedures to apply.
Is provisional credit always required?
No — it is required only when the institution cannot complete its investigation within ten business days and wants to use the extended 45- or 90-day window. If you resolve within ten business days, you correct the error (if any) within one business day and never issue provisional credit. Once you pass the ten-day mark unresolved, though, the credit becomes mandatory to keep the extension available.
What makes a transaction eligible for the 90-day window instead of 45?
Three classes under §1005.11(c)(3): transfers on accounts opened within thirty days before the disputed transaction (new accounts), point-of-sale debit-card transactions, and foreign-initiated transfers. These also carry a twenty-business-day base investigation rather than ten. Everything else uses the ten-business-day / forty-five-calendar-day standard track.
Does Reg E cover commercial ACH disputes or wires?
No. Regulation E governs consumer electronic fund transfers only. A commercial account dispute or a wire transfer falls under UCC Article 4A and the contractual timelines in your account agreement, which are different from the §1005.11 clocks. Misclassifying a commercial account as consumer starts a statutory clock the law never imposed and can distort your compliance metrics.
How do business days and calendar days differ in the deadlines?
The investigation windows (ten or twenty days) and the results notice (three days) are business days — they skip weekends and Federal Reserve holidays. The extension caps (forty-five or ninety days) are calendar days measured from the notice date. Mixing the two, or running business-day counts on a naive calendar, is the single most common way institutions miscompute a Reg E deadline.
Related guides in this collection
- Encoding Reg E Dispute Timelines in Python — the full banking-day deadline calculator for the 10/45/90-day clocks.
- Automating Provisional-Credit Decisions Under Reg E — deciding when credit is mandatory and computing the amount and reversal deadline.
- Reg E, NACHA & Audit Controls for Payment Reconciliation — the parent reference tying Reg E, NACHA, UCC 4A, SOX, and OFAC together.
- Modeling the Exception Lifecycle as a Finite-State Machine — the state machine a dispute flows through while its Reg E clock runs.
- Encoding Provisional-Credit State Transitions in Python — the guarded transition that issues and reverses provisional credit.