Provenance
Verifying receipts
Every paid-tier verification returns a signed receipt. Verifying it takes three inputs: (1) the receipt JSON, (2) our public JWKS, (3) an Ed25519 verifier. That's it — no vendor SDK, no phone-home.
The receipt shape
{
"email": "[email protected]",
"status": "valid",
"score": 94,
"checks": { ... },
"probed_at": "2026-07-15T09:41:53Z",
"kid": "v1",
"sig": "<base64url ed25519 signature>"
} Canonical bytes
We sign JSON.stringify of the receipt with
sig removed and keys
sorted alphabetically. Reformatting the JSON invalidates the
signature — always verify against the exact bytes you received.
Node.js
import { createPublicKey, verify } from 'node:crypto';
async function verifyReceipt(receipt) {
const jwks = await fetch('https://verify.zi2.app/.well-known/zi2cert.jwks').then(r => r.json());
const jwk = jwks.keys.find(k => k.kid === receipt.kid);
if (!jwk) throw new Error('Unknown kid');
const key = createPublicKey({ key: jwk, format: 'jwk' });
const { sig, ...rest } = receipt;
const canonical = Buffer.from(JSON.stringify(sortKeys(rest)));
const signature = Buffer.from(sig, 'base64url');
return verify(null, canonical, key, signature);
}
function sortKeys(o) {
const out = {};
for (const k of Object.keys(o).sort()) out[k] = o[k];
return out;
} Python
import json, base64, requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def verify_receipt(receipt):
jwks = requests.get('https://verify.zi2.app/.well-known/zi2cert.jwks').json()
jwk = next(k for k in jwks['keys'] if k['kid'] == receipt['kid'])
pubkey = Ed25519PublicKey.from_public_bytes(base64.urlsafe_b64decode(jwk['x'] + '=='))
payload = {k: v for k, v in receipt.items() if k != 'sig'}
canonical = json.dumps(payload, sort_keys=True, separators=(',', ':')).encode()
sig = base64.urlsafe_b64decode(receipt['sig'] + '==')
try:
pubkey.verify(sig, canonical)
return True
except Exception:
return False Browser (no dependencies)
Our public /verify-receipt
tool does exactly this using WebCrypto's SubtleCrypto
Ed25519 primitives. View source of that page for the ~40-line implementation.