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.
| file | required | The PDF (max 4 MB, not encrypted) |
| documentType | optional | e.g. payslip, invoice, bank-statement |
| reference | optional | Your reference, e.g. invoice number or EMP-0042/2026-09 |
| metadata | optional | JSON object of up to 10 extra string fields |
| format | optional | "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")); // trueResult statuses
| GENUINE | SHA-256 matches the stamped file the issuer signed (or the original, with a note). | |
| ALTERED | The file contains a NotChanged docId but its hash doesn't match: changed since issue. | |
| REVOKED | Hash matches, but the issuer has since been revoked. Treat with caution. | |
| NOT_FOUND | Not issued through NotChanged. Doesn't mean it's fake, only that it can't be verified. |
Errors & limits
| 401 | Missing, invalid or revoked API key | |
| 403 | Issuer revoked | |
| 400 | Malformed request | |
| 413 | File larger than 4 MB | |
| 422 | Not a PDF, unparseable, or encrypted |
Errors are JSON: { "error": "…" }.