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
- Fetch the JWKS from
https://app.verifyhuman.io/.well-known/jwks.json(cache it, and refetch when you meet an unknownkid). - Select the key whose
kidequalssignature.kid. - Canonicalise
receipt: JSON with keys sorted at every level, separators,and:with no whitespace, and non-ASCII characters escaped as\uXXXX(Python'sjson.dumps(receipt, sort_keys=True, separators=(",", ":"))). - 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.