This is the reference page for the engine. Eleven signal families run on every document; each one is described below with the same four headings — what it detects, the evidence it returns, its benign causes, and its severity logic. The benign-causes sections are not a disclaimer bolted on at the end. They are the part you need in order to build a review queue that your operations team does not learn to ignore.
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.
Severity vocabulary
Four severities, in a strict hierarchy. They describe how much a single finding establishes on its own, not how likely fraud is.
- high On its own establishes that the document changed after generation, or that a tool with editing intent was involved.
- medium A real deviation from "written once by one tool", with common benign explanations.
- low Weak or absence-based; removes a corroborating fact rather than adding one.
- info Context. Not counted in
signalCountand contributes nothing to the score.
One report contains at most one signal per family — that is a
report invariant. A family either fires once, with all its findings collected
into a single evidence object, or it does not appear at all.
Signals are sorted most severe first, then alphabetically by id, so reports are
byte-stable across runs.
1. Revision history
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/Prevchain 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.
- 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), andcontentObjectsOverwritten. - 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-fingerprintorfont-anomaliescarry 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,contentorimage.
2. Metadata
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.5andiTextagree. 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. - Evidence returned
-
A
mismatchesarray (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), andeditorRevealedByMismatch. - 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
/CreationDateand/ModDate, XMPxmp:CreateDateandxmp: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 bymetadata-mismatchinstead).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 PDFD:YYYYMMDDHHmmSSOHH'mm'syntax nor ISO-8601. - Evidence returned
-
A
findingsarray — 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
low medium high- What it detects
-
A tool whose purpose is to change page content appearing in the
document's production chain. Four source fields are checked — Info
/Producer, Info/Creator,pdf:Producerandxmp: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). - The reasoning is domain-specific: statements, invoices, payslips and certificates are emitted directly by server-side generators. A consumer editor in the chain means the file went somewhere it did not need to go. 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.
- Evidence returned
-
matches(which field, the raw value, the matched tool label and its category), the deduplicatedtoolslist, all four raw strings,revisions, andfullPageImagePages. - 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. medium for a fingerprint match with no structural corroboration in the same file. high when the match is corroborated — more than one revision, or at least one page whose background is a full-page raster image. That coupling is deliberate: a consumer editor plus independent structural evidence is a different claim from a consumer editor alone.
id-inconsistency
info medium- What it detects
-
Divergence in the trailer's
/IDarray. 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. Their divergence is the format's own machine-readable record that the file was saved again after creation. Tamperlens reads the/IDfrom the newest trailer that carries one, and reports every trailer's/IDas evidence. - Evidence returned
-
idOriginalandidCurrentas hex,trailerCount, andallTrailerIdsso 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
/IDentirely, 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. No
/IDarray at all: info — its absence removes the check rather than indicating a change, so it contributes nothing to the score and is reported only so that you know why the check is missing.
3. Page content
font-anomalies
high- What it detects
-
Two or more different subset prefixes for the same base font.
Embedded fonts are usually subsetted — only the glyphs the document needs
are included, and the name is prefixed with six uppercase letters and a plus
sign, as in
ABCDEF+Helvetica. A generator writing a document once collects every glyph it needs for a typeface into a single subset with a single prefix. Two prefixes for one typeface mean glyphs of that typeface were embedded on two separate occasions. - This is the classic "changed the numbers" trace, and its value is that it lives in the page resources rather than in metadata: it survives metadata stripping, and it survives a whole-file rewrite that flattens the revision history. Non-subset fonts are ignored — there is no prefix to compare.
- Evidence returned
-
conflicts(base font name plus the sorted list of prefixes found for it), the full list of subset fonts with raw names, andtotalFonts. - Benign causes
- Document assembly is the big one. Merge two PDFs from different sources and a typeface common to both arrives with two subsets — entirely innocent, and the reason a customer who combined two monthly statements into one upload will trip this. Signature and stamp annotations often bring their own font resources. Some producers emit a separate subset per page or per form XObject by design. A few tools re-subset on save and leave the old subset orphaned in the file.
- This family has no lower severity to fall back to, which makes it the single most important one to read alongside the rest of the report rather than alone. Two subsets in a two-page document whose revision history is clean and whose producer is a server library is most likely a merge.
- Severity logic
- high whenever at least one conflict exists; otherwise silent. There is no gradation — the observation is binary, and its strength comes from being hard to produce accidentally in a single-pass generator.
hybrid-page
info medium- What it detects
- A page whose background is a single raster image covering essentially the whole MediaBox, with text drawn on top of it. The distinction that carries the signal is text rendering mode. An OCR layer over a scan is drawn in mode 3 — invisible — so the page stays searchable while the scan is what you see. Visible vector glyphs over a full-page scan mean characters were placed on the page after the image was made, which is how values on a scanned document get altered.
- Detection requires walking each page's content stream, counting text-showing operators and splitting them into visible and invisible. A volume ceiling separates edits from design: above 25 visible text-showing operators the page is treated as a designed layout — a magazine page, poster or brochure with a full-bleed background image — and is not counted. The threshold was tuned against real full-bleed publication pages, whose visible-operator counts start around the low thirties for covers and run into the thousands for body pages. An overlay edit on a statement carries a single-digit count.
- Evidence returned
-
Per affected page: page number, object number, MediaBox width and height,
and the total, visible and invisible text-operator counts. Plus
plausibleOcrLayer, themaxOverlayTextOpsthreshold in force, anddesignedLayoutPagesIgnored— the pages that were excluded by the ceiling, so the exclusion is auditable rather than silent. - Benign causes
-
Scanned documents with OCR layers are the normal case, which is why that
shape is reported at
infoand contributes nothing to the score. Legitimate visible text over a full-page image does occur: a form template distributed as a scan and then filled in electronically, a date or page number stamped by a document-management system, a Bates number, a "COPY"/"DUPLICATE" watermark applied by the issuer, an electronically applied signature image with a typed name beside it. - Conversely, an absent signal here means little: an edit made inside the raster image itself, with no text layer added, leaves nothing for this family to see. Pixel-level image forensics is a different discipline and is not part of the current engine.
- Severity logic
- medium when at least one page has between 1 and 25 visible text-showing operators over a full-page image. info when full-page-image pages carry text but all of it is invisible — the standard OCR shape, reported for context. Silent when no page has both a full-page image and any text at all.
redaction-exposure
high medium- What it detects
- Text that somebody meant to remove and did not. Every other family here asks whether a document was changed; this one asks the opposite — whether a change the author intended actually happened. Two operations look identical on screen and are not remotely the same in the file: drawing a black rectangle over a name hides it from a reader and leaves every byte of it in the content stream, while deleting the text and then drawing the rectangle is redaction. Court filings, FOIA releases, regulatory submissions and merger documents have all been published with the first mistake.
-
Two findings are reported under this id. Unapplied redaction
marks is the unambiguous one: a
/Redactannotation is a request for redaction, and the specification is explicit that the content under it is removed only when a tool applies the mark. A file still carrying them was prepared for redaction and released before the final step — which reads to a recipient exactly like a finished redaction, because most viewers render the marks as solid boxes. Text under an opaque paint is the content-stream case: a run of text drawn first and then covered by a shape or image painted on top. - The discriminator for the second finding is paint order, and it is what keeps this from firing on ordinary documents. Table shading, highlight bands and coloured headers are all filled rectangles sharing coordinates with text — and every one of them is painted before the text, because that is the only way the text is visible. Only a rectangle painted after the text hides it.
- Evidence returned
-
For unapplied marks:
finding: "unapplied-redact-annotations",totalMarks, and the per-page mark counts inpages. -
For covered text:
finding: "text-under-opaque-paint",totalRuns,runsWithRecoveredText, per-page counts,coveredByFillandcoveredByImage,formXObjectsPresent, andmaxCoverPageFraction.recoveredcarries the words themselves, up to 120 characters per run, with the covering kind, box grey level and overlap. That is the point: if this engine can read them, so can anyone who selects the text or runs the file through any PDF library. Like every other report, it is returned to the caller and never stored — the API is stateless and this evidence lives exactly as long as the response. - Benign causes
- Images are the main one, which is why they are scored a band lower: a signature stamp, a logo or a pasted-in scan can legitimately land on top of a line without anybody intending to conceal it. A covering paint larger than 40% of the page is ignored outright as a background, watermark or stamp — redaction boxes are drawn tightly around what they hide, and without that bound a full-page DRAFT overlay would report every line on the page.
- A white or near-white box over text is more often a layout accident than an attempt to conceal, though it exposes the text just the same. Note also that this engine does not walk into Form XObjects: a covering box inside one is not seen, so on a document that uses them the count is a floor rather than a total, and the report says so.
- Severity logic
-
high for any unapplied
/Redactannotation, regardless of what the content stream looks like — the mark itself is the finding. high when at least one covering paint is a filled shape. medium when every cover is an image, which is materially weaker evidence and worth a look rather than an alarm. Silent when no text is covered and no marks remain.
active-content
medium- What it detects
-
Embedded JavaScript, automatic actions (
/OpenAction),/Launchactions, and attached files (/EmbeddedFiles). Static machine-generated documents carry none of these, so their presence means the file was authored or re-authored in a tool that adds interactive behaviour. The same four constructs are also the standard delivery mechanism for malicious PDFs, so this family does double duty as a document-fraud signal and a security signal. -
Detection is scoped to genuine dictionary-key positions in parsed
objects, never to raw byte matching. A
/JStoken that merely appears inside a page's content stream is page content — text that happens to look like an action — and cannot fire this family. Content streams are not scanned for these markers at all. Tamperlens flags what it finds and never executes, evaluates, extracts or opens any of it. -
/AcroFormalone does not fire this family. A form is not active content; it is reported in the evidence for context only. - Evidence returned
-
A boolean per construct (
javaScript,openAction,launch,embeddedFiles, plusacroFormfor context), a human-readablefoundlist, and up to 50containersgiving the flag, the dictionary key, the object number and the path at which each hit was found. AlsoscanScope: "dictionary-keys"andcontentStreamsScanned: false, so the detection boundary is stated in the report rather than assumed. - Benign causes
-
Interactive forms with field-validation or calculation scripts are entirely
normal, and many tax, government and insurance forms are full of them.
/OpenActionis commonly used for something as innocuous as "open at 100% zoom" or "jump to page 1". Portfolio PDFs and e-invoicing standards such as ZUGFeRD and Factur-X embed attachments by design — a legitimate XML invoice inside a PDF will fireembeddedFilesevery time. - In an encrypted document, dictionary keys stay readable but an action carried inside an encrypted object stream cannot be examined, so the list may be incomplete — the signal detail says so explicitly when that applies.
- Severity logic
- medium whenever any of the four constructs is present; otherwise silent. There is no escalation, because the fraud-relevance of active content depends almost entirely on document type — which is context only you have.
4. Signatures and structure
signature-coverage
info high- What it detects
-
Whether each signature's
/ByteRangeactually covers the whole file./ByteRangeis an array of offset/length pairs naming exactly which bytes a signature protects — normally everything except the hole where the signature value itself sits. Tamperlens computes the highest covered offset and compares it with the file length. Anything beyond it was appended after signing and is not protected by that signature. - This is the strongest structural finding in the engine, and it is arithmetic rather than cryptography: two integers, no certificate needed, no trust store consulted. It holds regardless of whether the signature's mathematics validate — which is exactly why it is worth reporting separately from validation.
- Evidence returned
-
Per signature: object number,
/SubFilter, the raw/ByteRangearray,coveredEnd,bytesAfterCoverage,coversWholeFile, and the total file size. PlusuncoveredCountand the document's revision count. - Benign causes
- The mechanism itself is a normal and intended part of PDF: a second signer, or a legitimate incremental save such as adding a permitted annotation to an already-signed document, produces bytes outside the first signature's range. PDF has a whole concept — DocMDP permissions — for expressing which subsequent changes a signer allowed. Tamperlens reports the coverage gap; it does not evaluate whether the change was permitted. Multi-signature workflows will therefore fire this legitimately.
- Two things this family explicitly does not do: verify signature cryptography, and validate the signing certificate or its trust chain. Those are separate jobs and a coverage report is not a substitute for either.
- Severity logic
-
No signature fields: silent.
high when at least one signature has a parsed
/ByteRangethat does not reach the end of the file. info when the document is signed and every signature spans the entire file — reported deliberately, as a positive fact worth having in the report rather than silence.
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 sortedcodeslist, up to 25 individual warnings with messages,xrefConflicts,trailingGarbageBytes,revisionChainBrokenandparseTruncated. - 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.
Encrypted documents: what the report tells you it did not do
When the trailer references an /Encrypt dictionary, the strings and
streams in the file are ciphertext. Metadata values, embedded font names and page
content streams cannot be read without the password, which Tamperlens does not
hold and never asks for.
The naive behaviour would be to run every signal family anyway and report whatever fires on the noise. Instead, the five content-dependent families are not run at all and the omission is disclosed as a first-class part of the report:
metadata-mismatchdate-anomaliesproducer-fingerprintfont-anomalieshybrid-page
The disclosure arrives as the structure-warnings signal, titled
"Encrypted document — content not analysed", carrying
evidence.code: "encrypted-content-not-analysed", the explicit
suppressedFamilies list, contentAnalysed: false and
structuralAnalysisRan: true. It rides on
structure-warnings rather than arriving as its own signal because of
the one-signal-per-family invariant.
Structural analysis is unaffected and does run. Revision count, the trailer
/ID pair, signature byte ranges and cross-reference consistency are
all readable without the password, as is the detection of action dictionaries —
dictionary keys are not encrypted either. Metadata that happens to read as plain
text still gets reported: a producer may leave the XMP packet in the clear via
/EncryptMetadata false, and a text-likeness gate lets that through
while dropping fields whose bytes are not readable text. Any fields dropped that
way are named in metadataFieldsSuppressed.
Read this carefully when triaging an encrypted file. The absence of a content-level signal in an encrypted document's report says nothing whatsoever about that document. Those checks did not run. Treat the report as structural-only rather than as a clean bill of health — and note that many issuers password-protect statements as a matter of routine, so encryption is not itself a signal.
How signals become a score
summary.riskScore is a weighted aggregation on 0–100, not a
probability and not a confidence. The rules are short enough to state
completely, which is the point — you should be able to reconstruct the number
from the signal list.
-
Weights.
high70,medium25,low10,info0. -
Floors. One
highsignal forces the score to at least 70; onemediumforces at least 30. - Diminishing returns. The first two signals of a given severity contribute their full weight; beyond that each further signal of the same severity contributes a sharply decaying fraction. A pile-up of weak findings converges rather than adding up indefinitely.
-
The high band is reserved. With no
highsignal present the score is clamped to 69, whatever the arithmetic says. SoriskBand === "high"is exactly equivalent to "at least one finding that on its own establishes a change". -
Bands. Below 30
low, 30–69elevated, 70 and abovehigh. -
signalCountcounts signals aboveinfo. An informational finding is visible in the report but invisible to the count and the score.
Weights are versioned by engineVersion: changing them is a version
bump, so a score you recorded last quarter still means what it meant. The engine
is deterministic — identical bytes produce an identical report, apart from the
per-call id — which means you can pin fixtures and regression-test
your own thresholds against us.
Using this honestly
Three habits that separate a useful screening step from an angry-customer generator:
- Baseline before you threshold. Run documents you believe are genuine through the engine, grouped by issuer, and look at which signals fire normally in your population. Producer strings and revision habits vary wildly between institutions.
- Report the fact, not the inference. "This file contains two revisions and names an online PDF editor" is defensible and specific. "Our system flagged this as fraudulent" is neither, and it is the sentence that ends up in a complaint.
- Correlate. A single medium signal is a question. A consumer editor plus a content-overwriting revision plus two subsets of one typeface is a coherent story, and the story is what a human reviewer can act on.
See the report on your own file
The free checker runs every family described 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 25 free documents a month.
Tamperlens reports risk signals, not authenticity verdicts. Signals can have benign causes; combine them with your own decision logic.
Related reading
- PDF metadata forensics Where each piece of evidence physically lives in the file format.
- How to detect if a PDF was edited The same checks by hand, with shell commands.
- Bank statement fraud signals Applying the field guide to lending, BNPL, proptech and onboarding flows.
- API quickstart & pricing Auth, both request forms, the report JSON annotated, quotas.