Dashboard

Evidence Receipts and JWKS

An Evidence Receipt is a signed, durable statement of a verification's result. Your server fetches it with your API key, checks the RS256 signature against VerifyHuman's public keys (JWKS), and can then rely on its fields: which product ran, in which environment, with what status and, for VerifyAge, which minimum age was enforced and whether it was met.

Receipts contain no images, document data or provider payloads.

Retrieve a receipt

curl "https://app.verifyhuman.io/api/v1/verifications/VERIFICATION_ID/evidence-receipt?clientKey=vhk_live_your_client_key"

Authentication. This endpoint reads the client key from the clientKey query parameter (or form field); it does not read X-API-Key. Call it only from your server, and keep such URLs out of logs you share.

Ownership. A receipt is returned only when the verification was made with the same API key. Any other verification — another key of yours, another account, or an unknown id — returns 404, so a 404 means "not yours": reject it. For hosted pages, use the key the page was created with; for pages with a VerifyFlow-managed key, confirm with the signed webhook instead.

Status Meaning
200 The signed receipt envelope
401 No valid key
404 {"error": "Not found"} Unknown verification, or not made with this key
404 {"reason": "receipt_issuance_missing"} A receipt was expected but not issued; contact support

Fetching a receipt is read-only and never billed.

The envelope

{
  "receipt": {
    "receipt_version": "vh-evidence-receipt-v1",
    "verification_id": "VERIFICATION_ID",
    "product": "verifyage",
    "environment": "live",
    "surface": "hosted",
    "status": "PASS",
    "proofs_satisfied": [],
    "reason_codes": [],
    "policy_versions": {},
    "timestamps": {
      "created_at": "2026-11-03T10:00:00Z",
      "completed_at": "2026-11-03T10:01:30Z",
      "receipt_generated_at": "2026-11-03T10:01:31Z"
    },
    "age_verification": {"minimum_age": 21, "requirement_met": true}
  },
  "signature": {
    "alg": "RS256",
    "kid": "KEY_ID",
    "value": "BASE64URL_SIGNATURE",
    "signed_at": "2026-11-03T10:01:31Z",
    "jwks_url": "https://app.verifyhuman.io/.well-known/jwks.json"
  }
}
Field Use
product verifyhuman, verifyage, verifyidentity or verifytrust: must equal the product you expect
environment live or test: require live for Live access
status The verification status (PASS, FAIL, …): require PASS to grant access
age_verification VerifyAge only: minimum_age enforced and requirement_met
reason_codes Safe reason codes (Error and Reason Codes)
timestamps.completed_at When the verification finished: check it is recent enough for your use

A receipt whose signature.alg is none (with available: false) is unsigned: do not rely on it.

Verify the signature

  1. Fetch the JWKS from https://app.verifyhuman.io/.well-known/jwks.json (cache it, and refetch when you meet an unknown kid).
  2. Select the key whose kid equals signature.kid.
  3. Canonicalise receipt: JSON with keys sorted at every level, separators , and : with no whitespace, and non-ASCII characters escaped as \uXXXX (Python's json.dumps(receipt, sort_keys=True, separators=(",", ":"))).
  4. Verify signature.value (base64url) as an RSASSA-PKCS1-v1_5 SHA-256 signature over those bytes.
import base64
import json

import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa

JWKS_URL = "https://app.verifyhuman.io/.well-known/jwks.json"


def _b64url(data: str) -> bytes:
    return base64.urlsafe_b64decode(data + "=" * (-len(data) % 4))


def public_key(kid: str):
    for jwk in requests.get(JWKS_URL, timeout=10).json()["keys"]:
        if jwk.get("kid") == kid and jwk.get("kty") == "RSA":
            n = int.from_bytes(_b64url(jwk["n"]), "big")
            e = int.from_bytes(_b64url(jwk["e"]), "big")
            return rsa.RSAPublicNumbers(e, n).public_key()
    raise ValueError("unknown kid")


def verify_receipt(envelope: dict) -> dict:
    receipt, signature = envelope["receipt"], envelope["signature"]
    if signature.get("alg") != "RS256":
        raise ValueError("receipt is not signed")
    if receipt.get("receipt_version") != "vh-evidence-receipt-v1":
        raise ValueError("unsupported receipt version")  # see "Receipt v2" below
    canonical = json.dumps(receipt, sort_keys=True, separators=(",", ":")).encode()
    public_key(signature["kid"]).verify(
        _b64url(signature["value"]), canonical, padding.PKCS1v15(), hashes.SHA256())
    return receipt  # raises InvalidSignature if it does not match

Use a receipt in an access decision

def accept_verifyage_result(envelope: dict, required_min_age: int) -> bool:
    receipt = verify_receipt(envelope)
    age = receipt.get("age_verification") or {}
    return (receipt["product"] == "verifyage"
            and receipt["environment"] == "live"
            and receipt["status"] == "PASS"
            and isinstance(age.get("minimum_age"), int)
            and age["minimum_age"] >= required_min_age
            and age.get("requirement_met") is True
            and not already_used(receipt["verification_id"]))  # your own store

Bind the receipt to your own transaction as well: for hosted pages, require the attempt your server started (Hosted Attempts API), and record each verification_id once.

Receipt v2

Accounts may also receive receipts with receipt_version vh-evidence-receipt-v2. They are signed the same way (RS256, kid, JWKS), but canonicalised with RFC 8785 (JSON Canonicalization Scheme) and carry richer blocks (decision, age_assurance, issuer). Check receipt_version and verify with the matching canonicalisation; reject versions you do not support.