Quickstart: Verify in the browser
You can check any HESO Action Receipt offline, in a browser or in CI, with @hesohq/verify-wasm. Verification runs locally. Nothing about it needs HESO, an account, or a network call.
A receipt is a signed JSON record of one action and the policy verdict that gated it. To verify it you re-run the same checks the signer ran. This page shows you how to do that in a few lines.
Install
npm install @hesohq/verify-wasmThe package is ESM and works in the browser and in Node.
Verify a receipt
Call init() once to load the WASM module, then pass verifyActionReceipt the raw receipt bytes — encode the receipt JSON with a TextEncoder. It returns a verdict with a verdict field and a trust_level.
import init, { verifyActionReceipt } from "@hesohq/verify-wasm"
await init() // load the wasm once, up front
// receipt is the Action Receipt JSON, exactly as you received it.
// The verifier takes the raw receipt bytes, not the object.
const bytes = new TextEncoder().encode(JSON.stringify(receipt))
const result = verifyActionReceipt(bytes)
console.log(result.verdict) // "Valid"
console.log(result.trust_level) // "L0" or "L1"A verdict of Valid means the receipt passed every check. trust_level is L0 for an operator-signed receipt, or L1 when a human approver also co-signed it. It is only meaningful when the verdict is Valid.
Detect tampering
This is the whole point of the verifier. The receipt’s content is bound to a BLAKE3 hash, and that hash is signed. Edit any field in content and the recomputed hash no longer matches, so the verdict flips.
// Change any field inside content...
const tampered = {
...receipt,
content: { ...receipt.content, amount: 999999 },
}
const bytes = new TextEncoder().encode(JSON.stringify(tampered))
const result = verifyActionReceipt(bytes)
console.log(result.verdict) // "HashMismatch" — the receipt no longer matches its hashA changed field gives you HashMismatch (the content no longer matches its hash). A broken or swapped signature gives you InvalidSignature. Either way, the receipt is rejected. You cannot edit a receipt and keep it valid without the signing key.
Why this works
Verification re-runs the same cryptography the signer used. It recomputes the BLAKE3 hash of the content and re-checks the Ed25519 signatures against it. None of this needs a server, so the verdict you get locally is the same one anyone else gets.
The verifier walks a fixed set of gates in order and stops at the first one that fails. The verdict names that gate: Malformed for a receipt it cannot parse, WrongAlgorithm for an algorithm it does not handle, HashMismatch for altered content, InvalidSignature for a signature that does not verify, and TrustLevelMismatch for a receipt that claims more than its signatures support. Some carry a :detail suffix, e.g. InvalidSignature:operator.
The trust_level is re-derived from the signatures actually present, not read as a claim. An operator signature alone is L0; an operator signature plus a human approver co-signature is L1. For the full gate order see Offline verification, and for every verdict and what it means see the Verdicts reference.
A Valid verdict proves the action was authorized under a known policy, and at L1 that a person approved it. It records what was authorized, not whether the action succeeded downstream.
If you would rather not write code, there is a hosted verifier at /verify that checks a receipt in the browser, and a heso-verify-cli for verifying receipt bundles from the command line.