Verdicts
When you verify a receipt you get back a verdict. Valid means it passed every gate. Any other value names the gate that failed.
Verification runs a fixed sequence of gates and stops at the firstone that fails, returning that gate’s name as the verdict. If every gate passes, the verdict is Valid and the re-derived trust level (L0 or L1) is meaningful. For the gate order, see Offline verification.
The Node and browser surfaces return an ActionVerdict whose verdict field is one of the values below. Success is the literal "Valid"; any other value names the failing gate. Some values carry a :detail suffix (for example WrongAlgorithm:plat). The Python CLI prints the same values in PascalCase with the trust level appended, like Valid (L0). Branch on the value; a failure never throws.
Verdicts
Every value the verifier can return. Split on : first to get the bare verdict, then read the suffix for the detail.
| Verdict | Meaning | What triggers it |
|---|---|---|
Valid | Every gate passed. | The bytes match their signature under a known policy. The re-derived trust_level (L0 / L1) is now meaningful. |
WrongAlgorithm | The alg is not the supported one. | alg is not heso-action/v2+ed25519 (e.g. a plat or witness envelope). May carry a :detail with the offending value. |
Unsupported | The action_version is not supported. | The receipt is structurally an ActionReceipt but its action_version is a format this verifier does not understand (newer, or a retired v1). Fails closed rather than mislabeling it as tampered. |
HashMismatch | The content was altered after signing. | The recomputed BLAKE3 of the canonical content does not equal the embedded action_hash. Signatures are not even checked. |
InvalidSignature | An Ed25519 signature does not verify. | The operator or an approver signature fails to verify against its role’s domain-prefixed canonical content. May carry a :detail from the signature check. |
SelfApproval | An approver co-signature is not an acceptable approval. | An L1 approver co-signature verifies but was produced by the same key as the operator. Both signatures check out, but an operator cannot approve its own action, so L1 is refused. |
ThresholdNotMet | A k-of-n quorum did not reach its threshold. | A k-of-n quorum receipt had fewer than threshold distinct, verified, approved approver legs. Carries have and need. |
MalformedRedaction | A redaction marker is malformed. | A redaction marker uses an unrecognized algorithm, or a commitment that is not 64 lowercase hex. The content and signatures are fine; the marker cannot be interpreted. |
TrustLevelMismatch | The claimed trust level is not supported by the signatures present. | The embedded trust_leveldisagrees with the level re-derived from the signatures that verified — e.g. the receipt claims L1 while carrying only an operator signature. Carries embedded and derived. |
Malformed | The receipt could not be parsed or structured. | The receipt is not a well-formed ActionReceipt — e.g. an unparseable body, or a signature set with no operator entry, a duplicate role, or an unknown role tag. May carry a :detail. |
TimeAnchorUnverifiable | A present RFC-3161 time anchor could not be verified. | The receipt carries a time_anchorthat does not verify against a pinned TSA root — a bad token, a wrong imprint, an untrusted root, or the surface (such as the browser) where TSA support is off. An absent anchor is not a failure. |
Because the verifier stops at the first failing gate, the verdict tells you more than what failed: it also tells you which gates already passed. An InvalidSignatureverdict means the algorithm and version were recognized and the hash matched — the bytes are intact, they were just not signed by the operator key. A HashMismatch therefore says nothing about the signatures: the verifier stopped before it reached them.
A Validverdict proves you authorized an action under your policy — the operator signed this exact action, and at L1 that a distinct person co-signed it with a device-held key. It does not prove the action succeeded downstream.