Comparing document hashes

A practical guide to comparing agreement text, notarized content, evidence metadata, and signed PDF hashes.

Use this page when a sealed PDF, email, blockchain transaction, or notarization gives you a hash and you want to compare it with the artifact you hold.

Agreement or declaration text

Normalize the exact body text in this order:

  1. Unicode NFC-normalize the text.
  2. Replace CRLF and CR line endings with LF.
  3. Strip leading and trailing whitespace.
  4. Encode the result as UTF-8.
  5. Compute SHA-256 and compare the lowercase hexadecimal result with the body hash.

Do not include the title, PDF layout, signature lines, or evidence appendix in the body hash unless those characters are part of the stored record body.

Reproducing a hash in code

The normalize-then-hash procedure above is easy to get slightly wrong by hand (a stray trailing space, the wrong newline convention), so here it is as code in three common languages. Each one takes the raw body text and produces the same lowercase hex digest Agreebase would store.

Python

import hashlib
import unicodedata

def agreebase_body_hash(raw_text: str) -> str:
    text = unicodedata.normalize("NFC", raw_text)
    text = text.replace("\r\n", "\n").replace("\r", "\n")
    text = text.strip()
    return hashlib.sha256(text.encode("utf-8")).hexdigest()

JavaScript

async function agreebaseBodyHash(rawText) {
  let text = rawText.normalize("NFC");
  text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
  const bytes = new TextEncoder().encode(text);
  const digest = await crypto.subtle.digest("SHA-256", bytes);
  return Array.from(new Uint8Array(digest))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

PHP

function agreebase_body_hash(string $rawText): string
{
    $text = Normalizer::normalize($rawText, Normalizer::FORM_C);
    $text = str_replace(["\r\n", "\r"], "\n", $text);
    $text = trim($text);
    return hash("sha256", $text);
}

Text notarization

Use the same NFC, LF, trim, UTF-8, and SHA-256 process. The result must match the notarization content hash. A text notarization can be checked with the public verifier only when the normalized text was made public or when you have the text and submit it for comparison.

File notarization

Take the exact file you believe was notarized and hash it yourself with SHA-256; the same tool the browser used (sha256sum, a hashing library, or the Python/JavaScript/PHP examples below). Don't hash a filename, a PDF rendering of the file, an exported metadata sidecar, or a screenshot; any of those will produce a different digest even if the content looks identical to you. Submit the resulting hash to /verify with the notarization's AGR reference. Agreebase never receives or stores the file bytes themselves, so this comparison is the only way to confirm the file matches; there's no server-side copy to fall back on.

Evidence metadata

Every sealed record comes with an evidence metadata package; a JSON document listing the record facts, the body/content hash, and each participant's signing evidence. That package has its own hash, so you can confirm it hasn't been altered independently of confirming the record body itself. To recompute it: take the metadata JSON, remove the metadata_hash field itself (it can't hash itself), re-serialize what's left with keys sorted and no extra whitespace, encode as UTF-8, and SHA-256 it. The result should match the sha256:-prefixed value stored in the package.

You won't normally need to do this by hand; it matters mainly if you're building an integration that re-verifies evidence packages programmatically, or if you want to confirm a package you were sent hasn't been tampered with before relying on it. The metadata hash is separate from the signed PDF hash: the PDF is its own artifact with its own document hash, not derived from the evidence package.

Blockchain transaction

Every sealed record includes a transaction hash in its confirmation email; the receipt for the write to Base. The easiest way to inspect it is basescan.org: paste the transaction hash into the search bar, open the transaction, and look at the "Input Data" field. That field holds exactly what Agreebase wrote: for a hash-only record, the AGR reference, the body or content hash, the evidence metadata hash, and the completion time, all as plain readable text (Basescan can decode it as UTF-8 if it doesn't do so automatically; look for a "Decode as UTF-8" or similar toggle near the Input Data section). If full-text publication was enabled, the agreement or declaration body appears there directly instead of just its hash.

This is the check that doesn't depend on Agreebase at all: the transaction is public and permanent on Base regardless of whether Agreebase's own servers are reachable. Compare the body or content hash shown in the calldata against your own recomputed hash, and the evidence metadata hash shown there against your own recomputed metadata hash, and you've verified the record independently of any single party's word for it. See Verification hashes for the exact technical model behind all of these, and Blockchain and permanence for what else the chain record does and doesn't contain.

Common mistakes

  • Hashing the PDF instead of the canonical text.
  • Including the evidence appendix in the body hash.
  • Using a different newline or Unicode normalization.
  • Hashing a changed file rather than the exact original bytes.
  • Treating a fuzzy fingerprint match as an exact match.
  • Comparing a superseded record with a newer current authority without checking the verification verdict.

Discard changes?

Are you sure? Your draft and any progress will be lost.