Z2 ZI2 Verify
Start free
Docs

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

JSON
{
  "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

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

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.