When a Truist statement prints separate Credits and Debits columns with a running Balance, convert each line to one signed amount (credits minus debits) and check it against the balance printed on that same line, chaining from the printed balance rather than your own running total. A single misread then flags exactly one row. ZeraBooks checks Truist statements line by line this way. (I founded ZeraBooks. The code below is plain Python.)
What does a Truist activity table look like?
In a Truist checking statement copy we've seen, activity sits in one table with the columns Date, Check #, Transaction Description, Details, Credits, Debits and Balance. Both amount columns print positive. Truist has more than one statement format, so check yours against it first.
It is friendlier than most banks, because every line carries a balance. It still breaks a spreadsheet in two ways. A SUM over two positive columns means nothing until you sign them. And the Details text under a transaction, such as the ACH company, comes out of most table extractors as a row of its own with no date and no amount.
How do you parse and check it?
The example is Piedmont Roofing Co., a fictional roofer with Truist business checking ending 3047, September 2026. The lines below are what a table extractor typically returns, one tuple per printed line. I planted one misread: check 4412 was really 1,085.00 and came out as 1,035.00.
from decimal import Decimal as D
BEGIN, ENDING = D("14210.65"), D("6862.28")
LINES = [
("09/02", "", "Deposit", "5,800.00", "", "20,010.65"),
("09/03", "4410", "Check", "", "2,145.00", "17,865.65"),
("09/09", "", "ACH Debit", "", "6,380.22", "11,485.43"),
("", "", "ABC Supply Co", "", "", ""),
("09/16", "", "ACH Deposit", "3,912.75", "", "15,398.18"),
("", "", "Stripe Transfer", "", "", ""),
("09/22", "4412", "Check", "", "1,035.00", "14,313.18"), # true value 1,085.00
("09/29", "", "ACH Debit", "", "7,450.90", "6,862.28"),
("", "", "ADP Payroll", "", "", ""),
]
def money(s):
return D(s.replace(",", "")) if s else D("0")
def rows(lines):
out = []
for date, chk, desc, cr, dr, bal in lines:
if not date: # a Details line: belongs to the row above
out[-1]["desc"] += " / " + desc
continue
out.append({"date": date, "check": chk, "desc": desc,
"amount": money(cr) - money(dr), "balance": money(bal)})
return out
def check(rs, begin, ending):
prev, bad = begin, []
for r in rs:
if prev + r["amount"] != r["balance"]:
bad.append(f'{r["date"]} {r["desc"]} {r["check"]}: '
f'{prev} + {r["amount"]} != {r["balance"]}, '
f'off by {prev + r["amount"] - r["balance"]}')
prev = r["balance"] # chain from the PRINTED balance
total = sum(r["amount"] for r in rs)
bad.append(f"{begin} + {total} = {begin + total}, printed ending {ending}")
return bad
for line in check(rows(LINES), BEGIN, ENDING):
print(line)
Output:
09/22 Check 4412: 15398.18 + -1035.00 != 14313.18, off by 50.00
14210.65 + -7298.37 = 6912.28, printed ending 6862.28
Change the 09/22 debit to 1,085.00 and the only line printed is 14210.65 + -7348.37 = 6862.28, printed ending 6862.28.
Why chain from the printed balance?
If you carry your own running total forward, one bad row poisons every row after it. Here, the 09/29 payroll line would also fail, and you'd have two suspects for one mistake. Resetting prev to the balance Truist printed means each line is tested on its own. The misread check is the only row flagged, and the 50.00 gap tells you which digit went wrong: a 3 read where an 8 was printed.
The end-of-month check still runs on your own total. It catches what the line check can't, like a whole line that never made it into the extract.
Does the corrected month add up?
Credits: 5,800.00 + 3,912.75 = 9,712.75. Debits: 2,145.00 + 6,380.22 + 1,085.00 + 7,450.90 = 17,061.12.
14,210.65 + 9,712.75 - 17,061.12 = 6,862.28, the printed ending balance. Line by line: 14,210.65 + 5,800.00 = 20,010.65, then 17,865.65, 11,485.43, 15,398.18, 14,313.18 and 6,862.28. Every one matches the Balance column.
What about the Details lines and check numbers?
The rows() function folds a dateless line into the description above it, so "ACH Debit / ABC Supply Co" stays one transaction. Keep the Check # in its own field rather than gluing it into the description. Check 4411 is missing from this month. That is normal for an uncleared or voided check, and it is far easier to spot in a sorted number column than in free text.
Two Truist-specific notes. Business accounts on account analysis get a separate Account Analysis statement that explains fees; don't feed it to this parser, because it isn't a list of transactions. And the account history download covers up to 18 months but usually has no balances, so none of the checks above can run on it. Use the statement PDF.
That is what a statement that doesn't tie looks like in ZeraBooks: the gap is named, not hidden. Download steps and a worked Truist month are on Truist bank statement to Excel. ZeraBooks reads Credits and Debits into one signed Amount, or keeps them as Money In and Money Out with the balance if you prefer. No bank feeds; you upload the PDF. It is 1,000 pages a month for $79 a month billed annually or $149 monthly, after a 7-day trial.
The balance column is free test data. Use every line of it.

Top comments (0)