When the numbers on the page disagree

Four signal families read the figures a document prints: check digits that do not add up, a balance column that stops following, and an amount sitting off the line it was printed on.

These four families read the document's own arithmetic and geometry rather than its structure — which is what makes two of them the only findings that survive a full re-save through another tool. A forger who launders a file through Ghostscript erases its revision chain and its producer string; the numbers still have to add up, and the glyphs still have to land where a generator would have put them.

Part of the field guide — four of the eighteen families the engine reports.

Signals, not verdicts. Every finding here is a structural fact about a file. "This document was modified after generation" is true or false about the bytes; "this document is fraudulent" is a judgement about a person, and Tamperlens does not make it. The API returns no boolean verdict field, by design.

br-identifier-checksum

high medium low
What it detects
A Brazilian identifier printed on the page whose own published check digit says it is wrong — a CPF, a CNPJ, an NF-e access key, or the 47-digit linha digitável of a boleto.
The boleto is the one worth the family. A payment slip's printed line is not free text: it is a fixed rearrangement of the 44-digit barcode, and its fourth field carries the mod-11 check digit of exactly the amount and due date the rest of the line encodes. Reconstruct the barcode from the printed line, run the check, and an amount somebody retyped contradicts arithmetic the issuing bank already performed — with nothing to compare it against. No barcode is decoded and no pixel is read.
That matters beyond boletos: it is one of the few findings here that survives a document being re-generated whole. A qpdf or Ghostscript rewrite launders every byte-level signal; it does not change what the digits on the page add up to.
Evidence returned
Per finding: the kind, the page, the shape it was recognised by, which component failed, the digit count and only the last four digits. For a boleto, the amountCentavos and dueDate the line encodes. The number itself is never republished — a CPF is personal data, and a report gets stored, logged and forwarded.
Benign causes
Typos and transcription. A failed CPF is more often a mistyped one than a fabricated one, which is why it is reported at low and never higher.
Deliberately invalid test numbers. Blank forms and templates carry 111.111.111-11 and friends by design; repeated-digit numbers are excluded outright.
OCR. A scanned page's text layer carries transcription errors that say nothing about fraud, so full-page-image pages are not read at all.
What it cannot see
This is not an identity check and not a lookup. No registry is consulted and nothing leaves the process. Knowing that eleven digits satisfy a published identity says nothing about whether they belong to anyone, or to the person named beside them.
A passing check digit is never reported, because it is worth nothing. Anyone can generate a valid CPF in a loop, and every template filler already does. Silence here is not a clean bill of health.
The barcode itself is not read. Comparing the printed line against the bars — the third carrier — needs a vector barcode decoder that is specified and not built; see docs/CORPUS-PHASE0-BOLETO-2026-08-05.md for the measurement that decided against it.
Severity logic
high when a boleto's printed line, in its canonical punctuated form, fails the general mod-11 check digit — the failure an edited amount or due date produces, and one a correctly-issued slip cannot have. medium when a single field's mod-10 digit fails (one mistyped block, which transcription also produces), when the same general failure appears in a bare digit run, or for an NF-e key. low for a CPF or CNPJ.
A bare 47-digit run is only read as a boleto on a page that carries at least three Febraban field names. Roughly one arbitrary digit run in ten clears the layout guard and then fails a check digit, and without that requirement the family would state it as "somebody edited a boleto".

running-balance-break

medium
What it detects
A balance column that stops following from the rows beside it. A running balance is a chain: each row's balance is the previous row's balance plus whatever that row records. A column that satisfies this five times in a row is a running balance, and a step it cannot account for is a step the numbers printed beside it do not explain.
This is the only family that can see a document generated cleanly from scratch. Every other family here asks whether a file was changed — a revision was appended, two dates disagree, a run is painted under a box — and all of them assume there was an original. A statement produced whole by a template filler, in one revision, with coherent metadata and one producer, passes all of them. What it does not do reliably is stay internally consistent, because the rows are invented one at a time and the chain constrains every one of them at once.
Evidence returned
Per chain: the page, how many rows and links it had, how many links held, how many broke, which link broke first, and how far the balance moved across it. Plus the number locale the document resolved to and how it was decided.
No page text is returned, and no balance. The movement is a difference of two balances — the finding — while the account's standing position is not something this engine copies into a report body somebody else will store and forward.
Benign causes
A balance carried forward. A chain that continues from a previous page or a previous statement legitimately starts from a number this page never shows.
A row we could not read. A fee line drawn as a picture, a figure with something painted over it, an amount in a form we do not walk — all of them are simply absent, and an absent amount looks exactly like a missing one.
Rounding and multiple currencies. The tolerance is two cents plus one basis point, which absorbs rounding but not a conversion.
What it cannot see
A scanned statement. There is no text layer to read and no OCR here, so the family says nothing at all — which is not the same as saying the document is fine.
A document whose number locale is ambiguous. 1.234,56 is one thousand in Brazil and one-point-two in the United States. The locale is decided once per document from its own separator usage, and a document that uses both conventions is declined rather than guessed at.
A forger who re-derived the column. Recomputing every balance after an edit defeats this entirely. The family raises the floor; it does not close the door.
Severity logic
medium, and deliberately no higher. An arithmetic finding reads like proof in a way a structural finding does not, so it is capped until measurement says otherwise. Measured across 3,842 published documents it fires zero times — and the same measurement found no real bank statement in that corpus, so its true-positive rate is demonstrated on fixtures and not on a population. Precision measured at zero fires is not precision measured high.
The precondition is the family: a column must hold at least five links and break at most twice. A table that is not a running balance breaks at nearly every link and is dropped in silence — 18 candidate chains exist in that corpus and not one of them is a statement.

glyph-anomalies

low
What it detects
A value that does not sit on the baseline of the text printed beside it, on a line that carries a money amount. A generator places every glyph of a printed line from one text matrix, so the line is flat. An editor that replaces a value cannot re-flow the page — it patches the content stream, re-derives the translation, and the replacement lands a fraction of a point out.
It is geometry, not structure. That is the only reason it is here: a revision count, an /ID pair and a producer string are all erased by re-saving the document through another tool, and where the glyphs land is not. It is one of the few findings that can survive a full re-distill.
Evidence returned
Per finding: the page, the baseline offset in ems, the number of runs on the line, and three booleans recording whether the same line also showed a font-subset change, explicit kerning inside a number, or a small size divergence. No page text is returned — the runs this family looks at are the amounts on the page.
Benign causes
Design and layout tools that place text element by element. Measured across 1,368 published documents this fires on 24 of them, and the producers are Canva, Quartz, Distiller, Word, pdfTeX and iText — every mainstream toolchain. There is no producer to exclude.
Superscripts, footnote marks and inline notation are excluded by construction: a raised run must be at least 85% of the size of the text beside it, which a superscript is not.
What it cannot see
Scanned documents. A scan's text layer is drawn in invisible render mode, so it is not text a reader sees and this family never reads it. About a third of both reference corpora draw no visible text at all, and silence on those is not a clean bill.
Anything about glyph widths. No font program is parsed and no /Widths array is loaded — a deliberate exclusion, since a font program is attacker-controlled binary. Only quantities the content stream states literally are read.
A value retyped alone on its own line. The check compares a run against its neighbours, so a figure with nothing beside it has no baseline to be off.
Severity logic
low, always, and the ceiling is measured rather than cautious. Across 1,006 forgeries paired against their own sources this finding appears on 43 and on 1 of the 160 sources; the operations that retype text fire it 6.6% of the time where their own source did not, and the 228 forgeries that retype nothing fire it 0.0% of the time. It also appears on 1.4% of US and 2.2% of Brazilian published documents, and it newly flags 3 of the 82 forgeries the rest of the engine misses. A severity of low carries no score floor, so this finding cannot on its own lift a clean document out of the low band — which is the honest weight for a residue that legitimate typesetting also produces. See docs/GLYPH-PHASE0-2026-08-05.md.

structure-warnings

low medium
What it detects
Everything the parser could not reconcile between the file's own bookkeeping and its actual bytes: cross-reference entries pointing at offsets that do not hold the object they claim, unreadable or reconstructed cross-reference sections, malformed subsections and entries, cross-reference chain loops, stream lengths that disagree with the stream, dangling references, and bytes trailing after the final %%EOF.
A correctly written PDF is internally self-consistent. Irregularities mean either a non-conforming producer or post-hoc manipulation of the bytes — and they also bound how much of the document the rest of the report could examine, which is why they are reported even when they look boring.
This family also carries the encrypted-document disclosure, described in full in the next section.
Evidence returned
warningCount, the deduplicated sorted codes list, up to 25 individual warnings with messages, xrefConflicts, trailingGarbageBytes, revisionChainBroken and parseTruncated.
Benign causes
Non-conforming producers are everywhere. Plenty of long-lived, entirely honest generators emit slightly wrong cross-reference offsets, because every real-world PDF reader repairs them silently and nobody ever noticed. Files that have travelled through email gateways, virus scanners or naive transfer paths can pick up trailing bytes. A truncated download produces a spectacular version of this signal and means nothing about the document's authenticity.
Severity logic
Silent when there are no warnings and the document is not encrypted. medium when there is at least one cross-reference conflict, or at least one warning whose code is in the serious set — xref-conflict, bad-offset, unparseable-xref, bad-xref-stream, bad-xref-subsection, bad-xref-entry, xref-chain-loop, xref-reconstructed, stream-length-mismatch — that is, the file's bookkeeping contradicts its bytes rather than merely being incomplete. low otherwise.

See the report on your own file

The free checker runs every family on this page and renders the full evidence — no account, nothing stored. To run it in your own pipeline, see the API quickstart or create an account for a key with 50 free documents a month.

Tamperlens reports risk signals, not authenticity verdicts. Signals can have benign causes; combine them with your own decision logic.

The rest of the guide