If your product accepts a bank statement as proof of income, address or balance, you have a document-fraud problem whether or not you have a document-fraud programme. This page is about the shape of that problem, and about a small, self-serve piece of it: reading what a PDF records about its own editing history and handing that back to your decision logic as JSON.
Why statements get forged
A bank statement is the most widely accepted, least verifiable document in consumer finance. It is asked for by lenders and BNPL underwriters as income and affordability evidence, by letting agents and proptech platforms as proof of rent affordability, by gig and marketplace platforms during payout onboarding, by neobanks as proof of address during KYC, and by brokers at refresh time. In most of those flows it arrives as a PDF the applicant uploaded, and in a lot of them nothing downstream ever compares it against the issuing bank.
That combination: high decision weight, self-supplied artefact, no authoritative counterparty. Is what makes it attractive. Open banking removes the problem where it is available and the applicant consents; the PDF upload path is the fallback that stays open, and the fallback is where the pressure goes.
The incentive is not always criminal-syndicate scale. A great deal of it is a single applicant, ten minutes before a deadline, who is £400 short of a threshold and has just discovered that a free online PDF editor will let them retype a number. That population is large, unsophisticated, and, for exactly that reason, detectable.
What naive statement fraud looks like
Sophisticated document fraud exists, and nothing on this page claims to stop it. But the everyday volume falls into a small number of recognisable patterns:
- Edit the real thing. Download the genuine statement from online banking, open it in a browser-based PDF editor, white out a figure, type a new one, download the result. Total elapsed time: two minutes. The editor writes its own name into the file's metadata and, depending on the tool, either appends an incremental update or rewrites the whole document.
- Print, alter, rescan. Or its digital equivalent: render the statement to an image, edit the image, wrap it back into a PDF. This defeats text-level checks and produces a file whose pages are full-page rasters. Very often the forger then wants the numbers to be selectable and adds a text layer, or the edit tool drops vector text on top of the image.
-
Template generation. Buy or build a statement template,
populate it with invented transactions, export. These files can look
immaculate on screen and carry entirely coherent metadata, because they were
genuinely generated once by a single tool. Structural forensics is weak here;
what betrays them is usually layout and typography, a different class of
check, or the one arithmetic this engine does run: a balance column that
stops adding up (
running-balance-break). - Reuse someone else's. The same PDF, resubmitted with a name changed, or an unaltered statement belonging to a different person. Nothing structural is wrong with the file at all.
- Sign, then modify. Where statements are digitally signed, common in parts of the EU and LATAM. Appending an incremental update after signing leaves the signature intact over the original byte range and the new content outside it. Some viewers show this clearly; users click through it.
Sophisticated document fraud exists and nothing on this page claims to stop it. The two dashed rows are where an honest tool says so rather than implying coverage.
The traces it leaves
A statement is a machine-generated document. That is the whole basis of the method: the file is supposed to have been written exactly once, by a server-side library, in one pass, with one set of metadata and one font subset per typeface. Every deviation from "written once by one tool" is a question you can ask the applicant.
-
More than one revision. Bytes appended after the first
%%EOF. Strongest when a later revision overwrites an object that already existed and that carries page, content-stream or image data: the rendered appearance changed after generation. - A consumer editor in the toolchain. Online converters, desktop PDF editors, image editors, office re-save pipelines. A core banking system does not emit statements through Photoshop.
- Two metadata stores that disagree. Info dictionary versus XMP packet. Editors frequently update one and forget the other, and sometimes only one of them names the editor at all.
- Two subsets of one typeface. Glyphs of the same font embedded on two separate occasions: the signature of text added to an existing document by a second tool.
- Visible vector text over a full-page scan. Legitimate scanned documents carry either no text layer or an invisible OCR layer. A handful of visible glyph runs over a raster page is how values on a scan get altered.
-
Bytes outside a signature's
/ByteRange. Content appended after signing, provable by comparing two integers, independent of whether the certificate validates.
Each of these is documented in full, including the benign explanations, which matter more than the malicious ones for anyone building a review queue, in the field guide. The manual version of the same checks is in how to detect if a PDF was edited.
Why enterprise tooling is out of reach
Document-fraud detection is a solved commercial problem at the top of the market. The established platforms are genuinely good and genuinely enterprise-shaped: a demo request form instead of a signup page, a sales cycle, an MSA, an annual minimum, and a per-document price that assumes you are processing at institutional volume. Several of them also involve a human-in-the-loop review step, which means latency measured in minutes and your applicants' documents sitting in someone else's review queue.
If you are three engineers at a seed-stage lender processing a few hundred statements a month, none of that is buyable. The usual result is that the upload endpoint ships with no document checks at all and the problem is rediscovered after the first loss.
Tamperlens is deliberately the small end of that market: sign up, get a key, make an HTTP call. No sales conversation, no minimum, no review queue. The trade-off is honest. It is a structural-forensics engine, not a full fraud platform, and it returns evidence rather than decisions.
Stateless by design. Uploaded bytes are parsed in memory and discarded when the response is written. No document is stored, no document is logged, nothing is sent to a third party, and there is no human review step. For a compliance team, "we never hold the file" is a materially easier conversation than a data-processing addendum about a review queue.
The API, in one call
One endpoint: POST /api/v1/inspect. It takes either
multipart/form-data with a file field, or a raw
application/pdf body, up to 10 MB. Authentication is a bearer
key. Free tier: 50 documents a month, no card.
multipart
curl -s https://tamperlens.com/api/v1/inspect \
-H "Authorization: Bearer tl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-F [email protected] | jq .
raw body, no multipart encoding, useful from a worker
curl -s https://tamperlens.com/api/v1/inspect \
-H "Authorization: Bearer tl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/pdf" \
--data-binary @statement.pdf | jq .
A response for a statement that was run through a free online editor looks like
this. detail strings are abridged here for width; in the real
response they are full paragraphs, and evidence carries the
offsets and object numbers behind each claim.
200 OK
{
"id": "insp_2f1e9c04-7b3a-4d51-9f0e-6c2a18d4bb71",
"engineVersion": "1.37.0",
"summary": {
"riskScore": 100,
"riskBand": "high",
"signalCount": 3,
"revisions": 2
},
"signals": [
{
"id": "incremental-updates",
"severity": "high",
"title": "Page content was replaced after the document was generated",
"detail": "The file contains 2 revisions. A later revision re-writes 2 object(s) that already existed in an earlier revision and that carry page, content-stream or image data (objects 12, 14). …",
"evidence": {
"revisions": 2,
"updatesAfterCreation": 1,
"eofOffsets": [118842, 184102],
"startxrefValues": [118201, 183640],
"revisionChainBroken": false,
"contentObjectsOverwritten": [
{ "obj": 12, "kind": "content" },
{ "obj": 14, "kind": "page" }
]
}
},
{
"id": "producer-fingerprint",
"severity": "medium",
"title": "PDF editing tool in the production chain (iLovePDF)",
"detail": "The document's metadata names iLovePDF in its production chain. Unlike a word processor or a design tool, these take an existing PDF as input and write a new one … The file also carries 2 revisions, so it was written more than once.",
"evidence": {
"tools": ["iLovePDF"],
"editorTools": ["iLovePDF"],
"origin": "pdf-editor",
"matches": [
{ "field": "Info:Producer", "value": "iLovePDF", "tool": "iLovePDF", "category": "online-editor", "origin": "pdf-editor" }
],
"producer": "iLovePDF",
"creator": "iText 7.2.5",
"revisions": 2,
"fullPageImagePages": []
}
},
{
"id": "id-inconsistency",
"severity": "medium",
"title": "Trailer /ID marks the file as changed since creation",
"detail": "The trailer /ID array holds two different identifiers. The first element is set once when a document is created and must never change …",
"evidence": {
"idOriginal": "8f2c...a91b",
"idCurrent": "41de...77c0",
"trailerCount": 2
}
}
],
"document": {
"pages": 2,
"producer": "iLovePDF",
"creator": "iText 7.2.5",
"creationDate": "2026-01-04T10:02:00.000Z",
"modDate": "2026-01-06T18:41:00.000Z",
"encrypted": false,
"signed": false,
"revisions": 2,
"sizeBytes": 184320
},
"disclaimer": "Tamperlens reports risk signals, not authenticity verdicts. Signals can have benign causes; combine them with your own decision logic."
}
The weights are versioned by engineVersion and the same bytes always produce the same score, which is what makes the number defensible in a file note. It is a weighted aggregation, not a probability.
riskScore is a weighted aggregation, not a probability. The
contract is deliberately narrow: any high-severity signal forces the score to
at least 70, and the high band is reserved for high-severity signals, a pile-up of weak findings is clamped at 69. So
riskBand === "high" means "at least one finding that on its own
establishes a change", which is a statement you can defend in a file note. The
weights are versioned by engineVersion; the same bytes always
produce the same score.
Full field reference, error codes and quotas: the API reference and the OpenAPI spec at /docs.
Wiring signals into a decision
The whole design assumes you own the decision. Some patterns that work:
Store the signal ids, severities and evidence objects against the application rather than the file: it is small, it is your audit trail, and it contains no document content.
-
Route, do not reject. Map
riskBandto a queue:lowcontinues automatically,elevatedgoes to manual review,highgoes to manual review with the evidence attached and a prompt for a fresh document. Auto-declining on a structural signal will decline real customers who re-saved a file in Preview. -
Store the evidence, not the file. Persist the signal ids,
severities and
evidenceobjects against the application. That is your audit trail, it is small, and it contains no document content. - Ask for a specific replacement. The single most effective response to a structural signal is "please re-download the original PDF from your bank and upload it without opening it in any other application". Genuine applicants can do this; the second submission is often more informative than the first.
-
Tune per signal, not per score. If your flow legitimately
involves customers signing statements,
incremental-updateswill fire constantly and you should weight it down for that flow while keepingsignature-coverageandfont-anomaliesat full weight. - Baseline your own traffic first. Run the checker or the free tier over a sample of statements you already believe are genuine, per issuing bank, before you set any threshold. Producer strings and revision habits vary enormously between banks, and knowing what "normal" looks like for your actual applicant population is worth more than any default we could ship.
What this does not do
Stated plainly, because a fraud tool that oversells itself is worse than none:
-
It does not read your statement for meaning. There is no
OCR and no name matching. The one arithmetic it does is the running
balance: since engine 1.25.0,
running-balance-breakreports a balance column that stops adding up, once a column has proved it is a balance, and says nothing about which number is wrong or why. It will not tell you that a closing balance disagrees with a total printed elsewhere, or that a transaction never happened. -
It does not verify identity or account ownership. A perfectly
genuine, untouched statement belonging to somebody else scores
low, correctly. - It does not detect a well-made template forgery. A file generated once, cleanly, from a fake template has nothing structurally wrong with it.
- It does not validate signature cryptography. It reports what a signature covers. Certificate and trust-chain validation is a different job.
- It does not survive a determined adversary forever. A forger who knows about revision history can flatten it. That is a real limit, and also why the signal set is versioned and grows: each new evasion technique leaves its own trace.
-
It does not issue a verdict. There is no
"fraud": truefield and there will not be one.
Try it on a real file first
Open the “Clean statement” and “Edited after creation” samples side by side, the same report the API returns, precomputed on a genuine-shaped statement and on the same statement after an editor changed a printed figure. That is the first row of the pattern figure above, with the evidence attached and a control to read it against. No account, no quota. Your own file goes through the same path: sent over HTTPS, parsed in memory, discarded when the response is written. Then get a key: 50 documents a month free, $29/mo for 1,000, $199/mo for 10,000, $749/mo for 50,000. Full terms on pricing.
Brazilian cards are charged in reais at checkout. These are the amounts you will be charged, not a conversion.
Tamperlens reports risk signals, not authenticity verdicts. Signals can have benign causes; combine them with your own decision logic.
Related reading
- PDF fraud signals field guide Every signal family, its benign causes, and how severity is decided.
- How to detect if a PDF was edited The manual checks, by hand, with shell commands.
- PDF metadata forensics Where the evidence physically lives inside the file format.
- What 190 published US bank PDFs look like inside A measured baseline: whether issuers leave a structural fingerprint at all.
- API reference Auth, both request forms, error codes, quotas.