What a PDF records about where it came from

Five signal families read what a PDF says about its own history: how many times it was saved, which tool wrote it, and whether its two records of itself agree.

A PDF carries its own paperwork: a revision chain, two sets of metadata, a producer string and a pair of identifiers. These five families never look at the page. They read that paperwork and check it against itself.

Part of the field guide. Five of the nineteen 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.

Every one of these five findings is a disagreement between two things the file says about itself: so all five are erased at once by a tool that rewrites the whole document.
TWO INDEPENDENT METADATA STORES trailer /Info XMP packet producer · creator producer · creator tool creation · modification create · modify dates a single tool writing once fills both consistently trailer /ID [ original, current ] the appended revision chain walked backwards through /Prev metadata-mismatch · date-anomalies producer-fingerprint id-inconsistency incremental-updates THE SHARED BLIND SPOT A tool that rewrites the whole file writes one fresh producer string, one fresh date pair, one fresh /ID and one revision. All five families go quiet, and the document reads as if it were written once.

incremental-updates

medium high
What it detects
That the file contains more than one revision. Bytes were appended after the document was first written out and terminated with %%EOF. PDF's incremental-update mechanism appends new objects, a new cross-reference section and a new trailer rather than rewriting the file, so earlier states of the document are still physically present. The parser walks the /Prev chain backwards and works out which object numbers each revision introduced and which it overwrote.
The escalation is about what changed, not how many times. If a later revision replaces an object number that already existed in an earlier revision, and that object carries page, content-stream or image data, then the rendered appearance of the document changed after generation. Annotation and metadata objects do not escalate.
The number of revisions decides nothing. What decides the severity is whether a later revision overwrote an object number that draws something.
THE FILE, FROM BYTE 0 TO THE END REVISION 1 objects 1-40 · xref · trailer %%EOF REVISION 2: APPENDED, NOTHING REWRITTEN only the objects that changed · new xref · trailer %%EOF /Prev: the chain the parser walks backwards WHAT REVISION 2 DID TO THE OBJECT NUMBERS IT WROTE wrote object numbers that did not exist before, or overwrote an annotation or metadata object medium overwrote an object number that already existed, and it holds page, content-stream or image data high: what a reader sees changed Signing, filling a form field and adding an annotation all append a revision. So does a document system on ingest.
Evidence returned
revisions, updatesAfterCreation, eofOffsets, startxrefValues, revisionChainBroken, a per-revision breakdown (changedPerRevision: which objects each revision wrote and which of those it overwrote, with the kind of each), and contentObjectsOverwritten.
Benign causes
Extremely common, and this is the family most likely to fire on an honest file. Applying a digital signature is an incremental update, that is how signing preserves the signed bytes. So is filling in an AcroForm field, adding an annotation, a comment or a sticky note, applying a redaction in some tools, and certain linearisation and optimisation passes. Document-management systems routinely add a revision on ingest. A statement that a customer opened, signed and re-saved is not a forgery.
The converse trap: a single revision does not mean the file was never edited. A tool that rewrites the whole document flattens the history, so this family stays silent while metadata-mismatch, producer-fingerprint or font-anomalies carry the finding instead.
Severity logic
Silent at one revision. medium for any additional revision. high when at least one overwritten object is of kind page, content or image.

metadata-mismatch

medium high
What it detects
Disagreement between the two independent metadata stores a PDF can carry: the trailer's Info dictionary and an embedded XMP packet. Four fields are compared: producer, creator/creator-tool, creation date and modification date. A single tool writing a file once fills both consistently; consumer editors very often update one and leave the other stale.
Comparison is deliberately forgiving so that version drift is not a finding. Tool strings are normalised to lowercase alphanumerics and count as agreeing when either is a substring of the other, so iText 7.2.5 and iText agree. Timestamps agree within one minute, because the two stores round differently. A field is skipped entirely when it is absent from either store, and the family does not run at all unless both stores are present.
Only one shape of divergence escalates: the one where the mismatch is the only reason an editing tool is visible in the file at all.
FOUR FIELDS, PAIRWISE producer iText 7.2.5 iText agrees modified 14:02:59 14:03:04 agrees /Info XMP Substring on the normalised string, one minute on a timestamp. Version drift is not a finding. WHAT SURVIVES THAT any surviving divergence two server libraries disagreeing, a stale XMP medium the two stores name different consumer editors including one naming an editor and the other naming none high Multi-stage publishing pipelines produce a plain divergence constantly: a design tool writes XMP, a distiller writes the Info dictionary, a post-processor updates one of them. A plain divergence is a question.
Evidence returned
A mismatches array (field, Info value, XMP value), both raw producer/creator strings from both stores, both date pairs, infoEditorTool / xmpEditorTool (which known consumer editor each store names, if any), and editorRevealedByMismatch.
Benign causes
Multi-stage publishing pipelines produce this pattern legitimately and constantly: a design application writes XMP, a distiller writes the Info dictionary, a post-processor updates one of them. Some server-side generators write XMP once and never refresh it. macOS Quartz and several print-to-PDF drivers are known to leave the stores divergent. A plain divergence is a question, not an accusation.
Severity logic
medium for any divergence. It escalates to high only in one specific case: the divergence is on a producer or creator field and the two stores name different known consumer editors, including the case where one names an editor and the other names none. In that situation the mismatch is the only reason the editing tool is visible at all, which is materially different from two server libraries disagreeing.

date-anomalies

low medium
What it detects
Timestamps that a correctly running generator could not have produced. Four distinct findings, evaluated across all four available date fields (Info /CreationDate and /ModDate, XMP xmp:CreateDate and xmp:ModifyDate):
mod-before-creation: the modification timestamp precedes the creation timestamp within the same store (the two stores are compared as separate pairs, so cross-store skew is handled by metadata-mismatch instead). future-date: a timestamp more than one day in the future. impossible-timezone: a declared UTC offset beyond ±14:00, which corresponds to no real timezone. unparseable-date: a string that follows neither the PDF D:YYYYMMDDHHmmSSOHH'mm' syntax nor ISO-8601.
Only one of the four findings is hard to explain away, and the thresholds on the other three are set wide enough that ordinary generator sloppiness does not reach them.
THE FINDING WHAT IT HAS TO SURVIVE SEVERITY mod-before-creation a tool copying a creation date forward medium future-date a wrong system clock, so the bar is a full day medium impossible-timezone nothing, no real zone is beyond ±14:00 medium unparseable-date legacy and niche generators, constantly low which is why a report of only these is downgraded All four are evaluated inside a single store. Skew between the Info dictionary and XMP is a different question, and metadata-mismatch answers it instead.
Evidence returned
A findings array: each with a kind, the field or field pair involved, the offending value and a one-line note, plus the four raw date strings exactly as they appear in the file.
Benign causes
Malformed dates are largely a generator-quality issue: plenty of niche and legacy producers emit dates that do not parse, and that is why a report containing only unparseable dates is downgraded rather than treated as manipulation. Future dates can come from a genuinely wrong system clock, which is why the threshold is a full day rather than a second. Mod-before-creation is the hardest to explain away benignly, but it does occur with tools that copy a creation date forward from a source document while writing a fresh modification date.
Severity logic
low when every finding is unparseable-date. medium otherwise. The title reflects the strongest finding: a mod-before-creation pair is named explicitly, malformed-only reports say so, everything else reports internal inconsistency.

producer-fingerprint

info low medium
What it detects
A tool a human drove appearing in the document's production chain. Four source fields are checked (Info /Producer, Info /Creator, pdf:Producer and xmp:CreatorTool), against a curated fingerprint list in five categories: online editors and converters (iLovePDF, Sejda, Smallpdf, PDFescape, PDF24, PDFfiller, DocHub, Soda PDF, PDF2Go, Convertio, Zamzar and others), desktop PDF editors (PDF-XChange, Foxit, Nitro, PDFelement, Wondershare, interactive Acrobat Pro), OCR post-processors (ABBYY, Readiris), image and design editors (Photoshop, Illustrator, GIMP, Inkscape, Canva, Figma, Affinity) and office/print re-save pipelines (LibreOffice, Word, Microsoft Print to PDF, Quartz PDFContext, CutePDF, doPDF, PrimoPDF).
Each fingerprint carries an origin, and the origin decides everything: an authoring tool (Word, Canva, Google Docs, a print driver, an OCR pass) is where a document's existence begins, so naming it says nothing about whether anything changed afterwards. A pdf-editor (iLovePDF, Sejda, PDF-XChange, Acrobat Pro's interactive editor) takes an existing PDF as input and writes a new one: a claim about the file's history rather than its authorship. Matching is case-insensitive on the alphanumeric-normalised string, so punctuation and version numbers do not defeat it.
A separate branch handles the opposite case: no producing tool declared in either store at all.
Two facts set the severity and nothing else does: where the document's existence began, and whether the file was saved more than once.
ONE REVISION SAVED AGAIN authoring tool Word, Canva, a print driver, an OCR pass the document begins here info scores nothing low a fact about the file, not a claim about it pdf-editor iLovePDF, Sejda, PDF-XChange: takes an existing PDF as input low medium a claim about the file's history, and still not about a person There is no high cell. A producer string is a self-report by the tool that wrote the file: a forger can set it to anything, and an honest document sets it to Word.
Evidence returned
matches (which field, the raw value, the matched tool label, its category and its origin), the deduplicated tools and editorTools lists, the resolved origin, all four raw strings, revisions, and fullPageImagePages, the page list is evidence only and no longer moves the severity.
Benign causes
Enormous, and this family needs local calibration more than any other. Legitimate reasons a consumer tool appears: the customer downloaded the statement and re-saved it in Preview or Acrobat to combine pages; they used an online tool to merge two statements into one upload, or to compress a file under an upload limit; they printed to PDF from online banking because no download button existed; the issuer itself uses LibreOffice or a print driver in its own pipeline. Small institutions genuinely do. ABBYY and other OCR tools appear routinely and legitimately in scan workflows.
The absent-producer case is weaker still: metadata stripping is a normal privacy-hygiene step, and some minimal generators simply never write the field. It removes a corroborating fact rather than supplying one.
Severity logic
No match, and a producer is declared: silent. low when no producing tool is declared anywhere. For a match, two facts and nothing else: the tool's origin, and whether the file has more than one revision. An authoring tool is info on a single-revision file and low on a re-saved one; a pdf-editor is low on a single-revision file and medium on a re-saved one. This family can no longer reach high: a producer string is a self-report by the tool that wrote the file. A forger can set it to anything, and an honest document sets it to Word. Full-page imagery no longer escalates it either; a full-page raster marks a document that is a picture (a Canva design, a scan), not a document that was edited.

id-inconsistency

info medium
What it detects
Divergence in the trailer's /ID array. The PDF specification assigns the two elements different jobs: the first is a permanent identifier set when the document is created and must never change; the second is rewritten by the producing application on every save. In a conforming writer their divergence is the format's own machine-readable record that the file was saved again after creation, but plenty of real writers simply generate both elements fresh on first output, so divergence alone is not that record. Tamperlens reads the /ID from the newest trailer that carries one, and reports every trailer's /ID as evidence.
A divergent pair is the format's own record that the file was saved again, but only on a file whose structure agrees it was written more than once.
trailer /ID [ 0, 1 ] element 0: permanent a1f3…9c set at creation, must never change element 1: rewritten on every save 7b02…4e so a conforming writer records the save here THE PAIR DIVERGES, AND THE FILE'S OWN STRUCTURE DECIDES WHAT THAT MEANS more than one revision medium: “saved again”, nothing more one revision info: a file written once was not saved again TWO WAYS THE TRACE SIMPLY IS NOT THERE Some minimal generators omit /ID entirely; a full rewrite sets both elements to the same fresh value. A matching pair is not evidence of an untouched document.
Evidence returned
idOriginal and idCurrent as hex, trailerCount, revisions, and allTrailerIds so you can see the identifier evolve across revisions.
Benign causes
Anything that legitimately re-saves a document updates the second element, signing, form filling, annotation. The signal says "saved again", nothing more. Two additional weaknesses worth knowing: some minimal generators omit /ID entirely, and a tool that rewrites the whole file can set both elements to the same fresh value, erasing the trace completely. A matching pair is therefore not evidence of an untouched document.
Severity logic
Identical pair: silent. Divergent pair: medium only when the file's own structure agrees it was written more than once. On a single-revision file the divergence is reported at info, a file written once cannot have been "saved again", and measured against uncurated document populations most divergent pairs sit on exactly such files. No /ID array at all is info: its absence removes the check rather than indicating a change. info findings contribute nothing to the score and are reported so the observation stays auditable.

See the revision chain on a document

Open the “Saved again, page changed” sample: a precomputed report where the second revision overwrote a content object, which is the incremental-updates escalation on this page with the evidence attached. Your own file goes into the same checker: it is sent over HTTPS, parsed in memory and never written to disk. No account, no quota. To run it in your own pipeline, see the API quickstart.

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

The rest of the guide