InvoanceInvoance
Log inStart free
In this article
Resources/How to Tell If a PDF Has Been Edited or Backdated (And How to Actually Prove It Wasn't)
Trust Infrastructure·11 min read·June 22, 2026

How to Tell If a PDF Has Been Edited or Backdated (And How to Actually Prove It Wasn't)

By Adeola Okunola, Founder, Invoance·

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:

The file's own dates are just editable strings
# 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.

A comparison of two ways to answer whether a PDF has been changed. On the left, forensic inspection after the fact, reading metadata dates, scanning incremental updates and xref, checking the producer and fonts, validating any embedded signature, which only detects careless edits and cannot prove integrity or origin time. On the right, cryptographic anchoring at the source, hashing the final bytes with SHA-256, signing and timestamping with Ed25519, recording in an append-only ledger, and re-hashing to compare, which proves integrity and origin time and is independently verifiable.
Forensics asks the file to be honest about itself. Anchoring removes the need to ask.

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.

The anchor and verify flow. Anchor once, when the document is final: take the final PDF, compute its SHA-256 fingerprint, POST it to /v1/document/anchor via API, SDK, or curl, and store a signed, timestamped record in an append-only ledger. Verify anytime, by anyone: take the PDF in question, re-hash it with SHA-256, and compare to the anchored record, producing a deterministic match (untouched, showing who and when) or no-match (the bytes were altered). A public verification page at www.invoance.com/proof/document/{id} requires no account and no API key.
Anchor once at the moment of authority; verify forever, with no account required.

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.

Node: hash a PDF and anchor it
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 record

Same 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.

Python: hash and anchor
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 record

The 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.

curl: POST /v1/document/anchor
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.

201 Created: POST /v1/document/anchor
{
  "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.

Node: verify a file against the anchor
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 file

The 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.

200 OK: POST /v1/document/{event_id}/verify
{
  "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.

Fetch a tenant's public key for offline verification
curl https://api.invoance.com/keys/yourco.com
# → {
#     "key_id": "key_01HXY…",
#     "algorithm": "ed25519",
#     "public_key": "base64url-encoded-32-byte-key…"
#   }
See it in action
  • 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.

Start freeDocument AnchoringDiscuss your use case
Adeola Okunola
Adeola Okunola

Founder, Invoance

About the author

I'm Adeola, founder of Invoance. I've spent most of my engineering life building systems where everything is provable. Invoance is what happens when you turn that obsession into infrastructure other people can use. Most "audit trails" can be quietly edited after the fact, which makes them stories, not proof. Most people use "evidence" and "proof" interchangeably. They aren't the same thing. I write here about audit integrity, AI attestation, and the gap between documenting controls and proving outcomes.

All articles by Adeola

Recommended

Product·7 min read

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.

Read
Trust Infrastructure·8 min read

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.

Read
Compliance·7 min read

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.

Read
Trust Infrastructure·11 min read

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.

Read

How to Tell If a PDF Has Been Edited or Backdated (And How to Actually Prove It Wasn't)

A PDF's CreationDate and ModifyDate are editable text, and forensic tools only catch careless edits, and a clean re-save erases the traces. Learn why you can't reliably detect a tampered or backdated PDF after the fact, and how to prove a document is byte-for-byte unchanged using a SHA-256 hash bound to an Ed25519 signature and a server-issued timestamp. Includes Node, Python, and curl you can drop into your own stack.

Category: Trust Infrastructure. Published 2026-06-22 by Adeola Okunola, Founder, Invoance. Tags: PDF Verification, Document Integrity, Tamper Detection, Backdating, Document Anchoring, SHA-256, Ed25519, PDF Forensics, Developer Guide, Cryptographic Proof.

Invoance

Neutral digital proof infrastructure for business. Tamper-evident, independently verifiable records.

Subscribe to our newsletter

Products
Platform
How It Works
Developers
Verify
Resources
Help & Legal
Products
  • Event Ledger
  • Document Anchoring
  • AI Attestation
  • Traces
Platform
  • Why Invoance
  • For Compliance Teams
  • For Finance Teams
  • Pricing
How It Works
  • Overview
  • Event Ledger
  • Document Anchoring
  • AI Attestation
Developers
  • Overview
  • Endpoints
  • Authentication
  • Concepts
Verify
  • Verify Document
  • Verify AI Attestation
  • Verify Event
  • Verify Trace
Resources
  • All Resources
  • SOC 2 Guide
  • HIPAA Guide
  • ISO 27001 Guide
Help & Legal
  • Support
  • Status
  • Verification Help
  • FAQ

Invoance provides technical verification and proof infrastructure for digital records. Invoance does not issue legal, financial, or regulatory advice.

Records anchored through Invoance are cryptographically signed and tamper-evident by design. Invoance does not verify the accuracy, legality, or authenticity of document contents, only that a record existed in a specific form at a specific time. Verification links are publicly resolvable and do not require authentication. Invoance does not act as a custodian of funds, a legal authority, or a regulated financial entity. Use of Invoance does not constitute legal compliance. Consult qualified counsel for your specific obligations.

© 2025 – 2026 Invoance, Inc. All rights reserved.••