Skip to content

Extracting Check Images and Numbers Referenced in a Bank Statement

8 min read · updated August 11, 2026

Checks are the one instrument on a bank statement with an identifier the account holder assigned, which makes them the easiest thing to reconcile and the easiest thing to reconcile wrongly.

Two places a check number lives

Banks reference a cleared check in one of two layouts, and some use both in one statement.

Inline in the transaction list. The description column contains something like CHECK 1043, CK#1043, CHK 1043 or simply 1043 in a narrow column of its own between the date and the amount. The inline form keeps the checks in date order alongside everything else, which is what you want for the balance identity, and it is the form most likely to be mangled: a bare four-digit number in a narrow column sits next to a dollar amount, and a table extractor that gets the column boundary slightly wrong will happily report a check number of 1043.00 or an amount of 1043.

A separate “Checks Paid” section. A block listing check number, date paid and amount, sorted by check number rather than by date, usually in two or three columns across the page to save space. This layout has two traps. Reading order across multi-column blocks is a coordinate problem rather than a text problem, so a naive text extraction interleaves the columns. And because the section is sorted by number, the dates run out of order, which breaks any code that assumes a statement’s rows are chronological.

Where both appear, the checks are usually in the summary section and not in the main transaction list, or vice versa. Extract both and deduplicate on check number, but count each check once when running the balance check from the opening and closing balance page, or every check will be counted twice and the residual will equal the total of all checks.

The asterisk means a gap

In the Checks Paid layout, banks conventionally print an asterisk or a hash next to a check number to mark a break in the sequence — that is, the previous number in the list is not the immediately preceding check number, because that check has not cleared, was voided, or was never written.

Checks Paid
   1041     03/02      120.00
   1042     03/05       45.00
   1046 *   03/09      310.00      <- gap: 1043, 1044, 1045 not cleared
   1047     03/14       88.20

That single character is the most information-dense mark on the page and it is exactly what an extraction tuned to pull “the number” discards. A missing check in a sequence is what a fraud review looks for and what an accountant chases at year end; the statement is telling you where to look and the parser is deleting it.

You can also derive gaps yourself by sorting the numbers, which is worth doing as a cross-check, but the two are not equivalent: the bank’s asterisk accounts for checks that cleared in a previous statement period, and your derived gap does not unless you have every statement. Extract the mark, store it, and compute your own gap separately.

The MICR line and a checksum you can run

Where the statement includes check images, the strip along the bottom of each check is the MICR line, printed in a magnetic machine font — E-13B across North America and much of the world, CMC-7 in France and parts of Europe and South America. Its fields are separated by dedicated symbols rather than spaces: the routing number sits between transit symbols, the account number and check number sit in the on-us field, and the amount is added at the bottom right by the first bank to process the item, which is why it is present on a cleared check and absent on a blank one.

The routing number is the useful part, because it carries a check digit you can verify. A US routing number is nine digits and satisfies a weighted sum with the repeating weights 3, 7, 1:

d = [0,1,2,3,4,5,6,7,8]     # the nine digits, left to right
w = [3,7,1,3,7,1,3,7,1]

total = sum(d[i] * w[i] for i in range(9))
valid = (total % 10 == 0)

# worked, on a synthetic number 122105155:
#   1*3 + 2*7 + 2*1 + 1*3 + 0*7 + 5*1 + 1*3 + 5*7 + 5*1
# =   3 +  14 +   2 +   3 +   0 +   5 +   3 +  35 +   5  = 70
#   70 % 10 == 0  -> structurally valid

This is worth building in for the same reason a container number or an ISBN is worth validating: it converts a nine-digit field where any OCR error produces a plausible result into one where nine out of ten single digit errors are caught for free. It does not prove the number belongs to a real institution — it is a structural check, not a lookup — but a failure is definitive evidence that something was misread.

One caution: the check number appears both in the MICR on-us field and printed in the top right of the check face, and the two can disagree — a misprint, or a check altered by hand. Banking systems key on the MICR value, so where you have both and they differ, take the MICR and flag the discrepancy rather than picking silently.

Check images on the statement

Image statements print four or six checks per page, fronts only or fronts and backs, reduced to a fraction of the original size. The consequences are predictable and specific. Handwritten payee names and memo lines are frequently below the resolution where they can be read at all, and a model asked for the payee will produce a plausible name rather than declining. The MICR strip usually survives, because E-13B is designed with thick, widely separated strokes for magnetic reading and degrades gracefully. And the endorsement on the back, when included, is a stamp overlaid on a printed background, which is the hardest thing on the sheet.

So the realistic target for an image page is: check number, amount, date, and routing and account fields from the MICR. Treat the payee as best-effort with an explicit confidence, and never let it silently overwrite a payee you already have from the transaction description.

What to store

Store the check number as a string. It has leading zeros on many accounts, those zeros are part of how the account holder numbers their checks, and an integer column destroys them irrecoverably. Store the source of each number — the inline description, the checks-paid section, or a MICR read from an image — because when two disagree you need to know which is which. Store the sequence-gap mark as a boolean of its own rather than leaving the asterisk inside the number string, where it will eventually be stripped by a cleaning step. And keep the routing and account numbers separate from the check number even though they share a printed field, since the on-us field layout varies by bank and the split is an interpretation you may need to revisit.