NotChanged

NotChanged API

Base URL: https://notchanged.com. Issuers sign PDFs with an API key; anyone can verify without one. NotChanged stores fingerprints (SHA-256) and signed records, never the documents themselves.

Authentication

Signing requires an issuer API key, sent as a bearer token. Keys are issued by NotChanged after we verify your platform or business, are shown once, and are stored only as a SHA-256 hash. Keys can be rotated or revoked at any time; a revoked key can no longer sign.

Authorization: Bearer dpk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

POST /api/v1/sign

Upload the original PDF as multipart/form-data. NotChanged computes the SHA-256 of the original, creates a record, signs it with the platform Ed25519 key, stamps the PDF with a QR verification badge, embeds the signed record as an attachment (docproof-record.json) and stores the SHA-256 of the exact stamped output.

filerequiredThe PDF (max 4 MB, not encrypted)
documentTypeoptionale.g. payslip, invoice, bank-statement
referenceoptionalYour reference, e.g. invoice number or EMP-0042/2026-09
metadataoptionalJSON object of up to 10 extra string fields
formatoptional"json" to receive JSON (also accepted as ?format=json)

Return the stamped PDF (default)

curl -X POST https://notchanged.com/api/v1/sign \
  -H "Authorization: Bearer $DOCPROOF_API_KEY" \
  -F "file=@payslip.pdf" \
  -F "documentType=payslip" \
  -F "reference=EMP-0042/2026-09" \
  -D headers.txt -o payslip-signed.pdf

# Response headers include:
# X-DocProof-Id: dp_...
# X-DocProof-Verify-Url: https://notchanged.com/v/dp_...
# X-DocProof-Sha256: <sha256 of payslip-signed.pdf>

JSON response

curl -X POST "https://notchanged.com/api/v1/sign?format=json" \
  -H "Authorization: Bearer $DOCPROOF_API_KEY" \
  -F "file=@invoice.pdf" -F "documentType=invoice"

{
  "docId": "dp_7k3m9x2q4r8t6w1z",
  "verifyUrl": "https://notchanged.com/v/dp_7k3m9x2q4r8t6w1z",
  "sha256Original": "…",
  "sha256Stamped": "…",
  "issuedAt": "2026-10-06T21:00:00.000Z",
  "record": { "v": 1, "type": "docproof.issuance", … },
  "signature": "<base64url Ed25519>",
  "registryRecord": { "type": "docproof.registry", "sha256Stamped": "…", … },
  "registrySignature": "<base64url Ed25519>",
  "keyId": "dp-ed25519-…",
  "pdfBase64": "JVBERi0xLjcK…"
}

Deliver the returned file unchanged: verification is byte-exact, so re-saving or re-compressing it will make it read as altered.

POST /api/v1/verify

Public, no key needed. Either upload the file, or hash it yourself and send only the hash.

# Upload (processed in memory, never stored)
curl -X POST https://notchanged.com/api/v1/verify -F "file=@payslip-signed.pdf"

# Hash-only (the file never leaves your machine)
curl -X POST https://notchanged.com/api/v1/verify \
  -H "Content-Type: application/json" \
  -d "{\"sha256\": \"$(sha256sum payslip-signed.pdf | cut -d' ' -f1)\"}"
{
  "status": "GENUINE",
  "message": "This document is genuine. It matches exactly what the issuer signed through NotChanged.",
  "sha256": "…",
  "match": "stamped",
  "document": {
    "docId": "dp_7k3m9x2q4r8t6w1z",
    "issuer": { "name": "Demo Payroll Ltd", "tier": "platform", "tierLabel": "Verified platform",
                "status": "active", "verifiedHow": "…" },
    "issuedAt": "2026-10-06T21:00:00.000Z",
    "documentType": "payslip",
    "reference": "EMP-0042/2026-09",
    "signatureValid": true,
    "keyId": "dp-ed25519-…"
  },
  "warnings": []
}

With a hash-only request NotChanged cannot look inside the file, so an altered file returns NOT_FOUND unless you also pass the docId printed on it.

GET /api/v1/me

Check an API key and see who it signs as (used by the Chrome extension's “Test connection”). A revoked key returns 401. A revoked issuer returns canSign: false.

curl https://notchanged.com/api/v1/me -H "Authorization: Bearer $DOCPROOF_API_KEY"

{ "issuer": { "id": "iss_…", "name": "Demo Payroll Ltd", "tier": "platform",
              "tierLabel": "Verified platform", "status": "active", "domain": "…" },
  "canSign": true, "limits": { "maxPdfBytes": 4194304 } }

CORS: /api/v1/sign and /api/v1/me accept cross-origin requests only from browser extensions (chrome-extension://). /api/v1/verify and document records are open to any origin.

GET /api/v1/documents/:docId

The public registry entry for a document, including both signed records, for independent verification.

curl https://notchanged.com/api/v1/documents/dp_7k3m9x2q4r8t6w1z

Public keys & checking signatures yourself

Keys are published at /.well-known/docproof-keys.json (JWK + PEM, with kid). A signature is Ed25519 over the UTF-8 canonical JSON of the record (keys sorted lexicographically, no whitespace), encoded base64url.

// Node.js
import { createPublicKey, verify } from "node:crypto";
const keys = await (await fetch("https://notchanged.com/.well-known/docproof-keys.json")).json();
const doc  = await (await fetch("https://notchanged.com/api/v1/documents/" + docId)).json();
const canon = (v) => Array.isArray(v) ? "[" + v.map(canon).join(",") + "]"
  : v && typeof v === "object" ? "{" + Object.keys(v).sort().filter(k => v[k] !== undefined)
      .map(k => JSON.stringify(k) + ":" + canon(v[k])).join(",") + "}" : JSON.stringify(v);
const key = createPublicKey(keys.keys.find(k => k.kid === doc.keyId).publicKeyPem);
verify(null, Buffer.from(canon(doc.registryRecord)), key, Buffer.from(doc.registrySignature, "base64url")); // true

Result statuses

GENUINESHA-256 matches the stamped file the issuer signed (or the original, with a note).
ALTEREDThe file contains a NotChanged docId but its hash doesn't match: changed since issue.
REVOKEDHash matches, but the issuer has since been revoked. Treat with caution.
NOT_FOUNDNot issued through NotChanged. Doesn't mean it's fake, only that it can't be verified.

Errors & limits

401Missing, invalid or revoked API key
403Issuer revoked
400Malformed request
413File larger than 4 MB
422Not a PDF, unparseable, or encrypted

Errors are JSON: { "error": "…" }.