How to Tell If a PDF Has Been Edited or Backdated (And How to Actually Prove It Wasn't)
A PDF's Created and Modified dates are just editable text, and a clean re-save erases the very traces forensics looks for. You cannot reliably detect tampering or backdating after the fact. Here is why, and how to prove a document is unchanged with a hash, a signature, and a timestamp its author never controlled.
A PDF's dates are a claim, not a fact
Open any PDF's properties and you will see a Created date and a Modified date. They look authoritative. They are not. Those values live in two places inside the file, the /Info dictionary and the XMP metadata stream, and both are plain, writable text. The file is asserting when it was made. Nothing is enforcing that the assertion is true.
Changing them takes one line and a free tool that ships on most machines:
# Rewrite what the PDF claims about itself:
exiftool -CreateDate="2024:01:05 09:14:00" \
-ModifyDate="2024:01:05 09:14:00" report.pdf
# The file now swears it was created 18 months ago.
# Nothing stopped it. Nothing recorded the change.What forensic inspection can, and can't, tell you
When the metadata can't be trusted, the next move is forensics: inspect the file's structure for signs of editing. This is a real discipline, and it works often enough to be worth doing.
Many editors append to a PDF rather than rewriting it, so each save can leave a trail: multiple end-of-file markers, a growing cross-reference (xref) chain, more than one /Producer string, fonts or objects that don't match the rest of the document. Tools like pdfid, Didier Stevens' pdf-parser, qpdf, and exiftool surface these. A CreationDate that falls after the ModifyDate, a /ID that was touched, three generations of incremental updates on a document that should have one. These are tells, and a careful analyst will find them.
But forensics has a ceiling, and it is lower than people think. Every one of those tells disappears the moment someone re-saves the file cleanly: linearize it with qpdf, distill it through Ghostscript, or simply "print to PDF." That collapses the edit history into a single pristine generation and normalizes the metadata. What is left looks untouched because, structurally, it now is.
So forensic inspection can catch the careless. It cannot clear the careful, and it can never establish what a file's true creation date was. You are still trusting the document to incriminate itself, and a competent editor will not let it.
Key insight. Forensics can catch a sloppy edit. It cannot prove a clean file is authentic, and it cannot recover a true creation date. Absence of tamper evidence is not proof of integrity.
Stop interrogating the file. Anchor it instead.
There is a structural reason after-the-fact detection keeps failing: you are trying to reconstruct the past from an artifact that anyone could have rewritten. No amount of inspection fixes that, because the information you need, what the file was and when, was never independently recorded.
The fix is to record it once, at the moment the document becomes authoritative. Take a cryptographic fingerprint of the exact bytes, bind it to a timestamp you did not write and a signature only your organization can produce, and store that where it cannot be quietly edited later. Do that, and the two hard questions collapse into easy ones. "Has it been edited?" becomes a hash comparison. "Was it backdated?" becomes a lookup against a timestamp the file's author never controlled.
This is the difference between forensics and provenance. Forensics asks a suspect file to confess. Provenance means you already hold an independent, signed record of the truth, so the file has nothing left to lie about.
How Invoance answers it
Invoance's Document Anchor is that record, and the flow is deliberately small.
When a document is final (a signed contract, an issued invoice, a filed disclosure), you compute its SHA-256 hash and send it to the anchor endpoint. Invoance stamps it with a server-issued UTC timestamp, signs the record with your tenant's own Ed25519 key, and writes it to an append-only ledger. You get back an event ID and a public verification URL. The document itself never has to leave your infrastructure: the proof is built from the hash, so hash-only is the default and anything sensitive stays with you.
Three properties make the result trustworthy rather than merely stored. The hash is brittle by design: change a single byte of the PDF and it no longer matches. The timestamp is issued by the service, not copied from the file, so it answers "when" without depending on the document's self-reported dates. And the signature is made with a key unique to your organization and checkable against your verified domain, so a third party can confirm both integrity and origin without taking your word for any of it.
A note on language, because it matters here: the ledger is append-only and signed, which makes it tamper-evident and independently verifiable. That is the precise, defensible claim, not a hand-wave that bytes are magically unchangeable.
Anchor a PDF from your stack
The integration is one call at the point a document becomes authoritative. Hash the bytes locally, then anchor the hash. Here it is in Node, Python, and plain curl.
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { InvoanceClient } from "invoance";
const client = new InvoanceClient(); // reads INVOANCE_API_KEY from env
// 1. Hash the final bytes locally; the document never has to leave
const bytes = await readFile("./invoice-1042.pdf");
const documentHash = createHash("sha256").update(bytes).digest("hex");
// 2. Anchor the hash at the moment the document is authoritative
const anchor = await client.documents.anchor({
documentHash,
documentRef: "Invoice #1042",
metadata: { issuedBy: "billing@yourco.com", matter: "ACME-MSA" },
});
console.log(anchor.event_id);
// → store this alongside your own recordSame anchor, Python and curl
For data and back-office pipelines, the Python SDK exposes the same call. And because anchoring is a single authenticated POST, you can do it from a shell script or any language with curl.
import hashlib
from invoance import InvoanceClient
async with InvoanceClient() as client: # reads INVOANCE_API_KEY
raw = open("invoice-1042.pdf", "rb").read()
document_hash = hashlib.sha256(raw).hexdigest()
anchor = await client.documents.anchor(
document_hash=document_hash,
document_ref="Invoice #1042",
metadata={"issued_by": "billing@yourco.com", "matter": "ACME-MSA"},
)
print(anchor.event_id) # store alongside your own recordThe raw request and what comes back
No SDK required. Hash the file, POST the hash. If you would rather not hash by hand, the SDKs also expose a documents.anchorFile / anchor_file helper that reads the file, hashes it locally, and anchors in one step. Either way the hash is computed on your side. Sending the document bytes is optional, and only happens if you explicitly ask Invoance to retain the original for later retrieval.
DOC_HASH=$(sha256sum ./invoice-1042.pdf | cut -d' ' -f1)
curl -X POST https://api.invoance.com/v1/document/anchor \
-H "Authorization: Bearer $INVOANCE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"document_hash": "'$DOC_HASH'",
"document_ref": "Invoice #1042"
}'What you store
The response carries the event ID you keep next to your own record, the server-issued timestamp, and the exact hash that was anchored. Anchor the same hash twice and the API tells you it already exists rather than writing a duplicate, so it is safe to wire into a retry.
{
"event_id": "9549c332-a52b-4f7e-9b1a-7d2e6c0f4a8b",
"created_at": "2026-06-22T14:09:51.204Z",
"document_hash": "a94a8fe5ccb19ba61c4c0873d391e9879823a4e0…",
"status": "accepted"
}Verify it later: three ways, no trust required
Anchoring is only half the value. The other half is that anyone (you, a counterparty, an auditor, a court) can check a file against the anchor, and the check does not require trusting you.
The first way is programmatic. Hash the file you are holding and POST it to the verify endpoint. You get a definitive match or no-match, plus the server timestamp and your verified issuer identity. A match means byte-for-byte identical to what was anchored.
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { InvoanceClient } from "invoance";
const client = new InvoanceClient();
const inHand = await readFile("./invoice-they-sent.pdf");
const documentHash = createHash("sha256").update(inHand).digest("hex");
const result = await client.documents.verify("9549c332-a52b-4f7e-…", {
documentHash,
});
console.log(result.match_result); // true → identical to the anchored fileThe verify response: integrity, time, and issuer
The same call over curl returns the match result, both hashes for side-by-side comparison, the moment the record was anchored, and the organization that issued it, including whether the issuing domain is verified. That issuer block is what lets a recipient trust who anchored the file, not just that it matches.
{
"event_id": "9549c332-a52b-4f7e-…",
"match_result": true,
"document_ref": "Invoice #1042",
"anchored_hash": "a94a8fe5ccb19ba61c4c0873d391e9879823a4e0…",
"submitted_hash": "a94a8fe5ccb19ba61c4c0873d391e9879823a4e0…",
"anchored_at": "2026-06-22T14:09:51.204Z",
"organization": {
"name": "YourCo, Inc.",
"issuer_name": "YourCo Billing",
"primary_domain": "yourco.com",
"domain_verified": true
}
}Verify with no account, or fully offline
The second way needs no account and no code. Every anchor has a public page at www.invoance.com/proof/document/{event_id}. A recipient drops the file in, their browser hashes it locally with SHA-256, and the page tells them whether it matches what your organization anchored, and shows your verified domain as the issuer. This is the link you put in an invoice footer or a contract's signature block.
The third way is fully offline. Each tenant has its own Ed25519 keypair, and the public key is published by your verified domain. A verifier who wants to depend on nothing, not even Invoance being online, can pull the key and check signatures on your anchored records with any standard crypto library.
curl https://api.invoance.com/keys/yourco.com
# → {
# "key_id": "key_01HXY…",
# "algorithm": "ed25519",
# "public_key": "base64url-encoded-32-byte-key…"
# }- Document verification flow— The public proof page, the verify endpoint, and how a third party checks a document with no Invoance account.
- Documents SDK reference— Runnable anchor, verify, get, and list calls for the Node and Python SDKs, with request and response shapes.
Backdating, specifically
Backdating deserves its own paragraph, because it is where the file's self-reported dates do the most damage. A CreationDate is just a string. Set it to any moment in the past and the file will repeat that claim forever.
An anchor answers the question a different way. The timestamp on the record is issued by the service at the instant you anchor, signed, and written to an append-only ledger, none of which the document's author can reach or rewrite. The anchor does not ask when the file says it was made. It states when the file demonstrably already existed in this exact form.
There is an honest limit here, and it is worth stating plainly: an anchor proves a document existed, unchanged, no later than the moment you anchored it. It does not retroactively prove what happened before that. Anchor a contract the day it is signed and the timestamp is the signing date for every practical purpose. Anchor it a year late and you have proven it existed by then: useful, but weaker.
Key insight. Anchor at the moment a document becomes authoritative. An anchor proves a file existed, unchanged, no later than its timestamp, so the earlier you anchor, the more it proves.
What an anchor proves, and what it doesn't
Precision is what makes this credible, so here is the exact line.
An anchor proves four things: the exact bytes of the document at anchoring time; that those bytes are unchanged whenever you re-check them; the moment the record was sealed, on a timestamp the author did not control; and which organization issued it, tied to a verified domain.
It does not prove the document is true, that its contents are accurate, that the right person authored it, or that anything happened before the anchor. A backdated lie that gets anchored is still a lie, now with a precise record of when you anchored it. Anchoring secures integrity and provenance. It does not adjudicate truth, and any vendor claiming otherwise is overselling.
Where to start
You do not anchor everything. You anchor the documents where the cost of not being able to prove integrity is highest, and you do it where they are generated.
Start with the obvious targets: executed contracts at signing, invoices and wire instructions at issuance, regulatory filings at submission, policies and board minutes at approval. Invoices and payment instructions deserve special mention: if every legitimate one carries a verification link, a recipient can reject a spoofed or altered copy in seconds, which quietly defeats a whole class of business email compromise.
Then publish the convention so people actually check: a verification URL in the footer of outbound invoices, an anchor hash in the contract signature block, a verify link in your statement-of-work template. The technical work is one API call. The leverage is making "verify before you act" the default.
The activation path is short. Create an organization, generate an API key, install the SDK, and your first anchor returns a real event ID and a real public verification URL on the free tier. The fastest way to believe this is to anchor one PDF, change a single byte, and watch the verify endpoint flip from match to no-match.
Cryptographic proof that a document existed, unchanged, at a specific time, verifiable by any third party.
Recommended
Introducing Document Anchor: Cryptographic Proof That a Document Existed, Unchanged, at a Specific Moment
Contracts get disputed. Filings get questioned. Wire instructions get spoofed. Document Anchor replaces 'trust our DMS' with cryptographic proof anyone can verify, and breaks the BEC playbook in the process.
Document Anchoring: Cryptographic Proof for Business Records
Every business depends on documents, contracts, invoices, certificates, audit reports. Document anchoring creates cryptographic proof that a specific document existed in a specific form at a specific time, without relying on the integrity of any single system.
Why Traditional Audit Logs Fail Under Regulatory Scrutiny
Your application logs record what happened. But in an audit or legal proceeding, the first question is not what your logs say, it is whether anyone can trust your logs. Traditional logging has a fundamental integrity problem that most teams do not address until it is too late.
Trust Infrastructure: What Compliance Automation Cannot Prove
Compliance automation tells auditors what controls you have. Trust infrastructure proves what actually happened. As regulatory scrutiny intensifies and AI systems scale, the gap between documenting controls and proving outcomes is becoming the most expensive blind spot in enterprise security.