How a Relying Party Verifies an Agent Credential, and Why `valid` Is Not the Field You Want
What arrives at your door
One header. Inside it, a selective-disclosure proof, the issuer's public key, the messages the agent chose to disclose, a timestamp, and optionally a status reference and a credential identifier.
Verification is offline by default. Checking the proof itself needs no call to us and no account with us.
What a passing check establishes
That somebody presented a valid proof over messages signed by that issuer key, bound to the request method, the request path and that timestamp.
And not that the subject participated. The bundle behind the proof is a bearer credential: there is no holder key binding anywhere, so possession is sufficient to present. Write your logs and your copy as "the presenter possessed a valid credential", never as "the holder proved". The difference matters the first time somebody disputes a transaction.
The field everyone reads is not the field that answers the question
valid means the proof is valid and, when checkable, not revoked. The qualifier is the whole
problem.
With no status reference in the envelope, the result is valid: true with revoked: null. The
credential could have been revoked an hour ago and nothing in that verdict would know.
And the status reference is put there by the presenter. The presenting side decides whether to
include it. So a check that reads only valid lets the party being checked decide whether
revocation is checked.
That is the sentence to design against.
The library tells you, and this deserves credit
revoked is a tri-state, not a boolean. Its own documentation says that null means no status
reference was supplied and revocation was not checked.
verifiedAsOf and staleMs are null in the same case, so a verdict carries the age of the
snapshot it rests on, or an explicit absence of one.
The information is right there. The trap is that valid is the headline and the tri-state is the
fine print, which is a naming problem rather than a missing guard.
And where it does check, it fails closed
Supply a status reference without the issuer's status-signing key and the result is invalid, with a reason saying the status was unavailable. It does not assume fine.
A status list whose signature does not verify is its own distinct reason, separate from revoked.
So broken and bad are not conflated, which is the discipline that keeps people from learning to ignore a red light. The failure enumeration is the evidence, and it is the reason this page can recommend the strict path without hand-waving.
The authoritative path cannot be bypassed by omission
Live verification asks the chain, which checks the proof against the anchored record and returns the revocation state it holds.
A missing credential identifier fails there rather than passing, so the omission trick that works against a lenient cached check does not work here.
The cost is a network call and an exception to handle, and for anything consequential that is the right trade.
Qualifier added 2026-08-02, from the page written next to this one. Preferring the live path is right about bypass-resistance and wrong as a blanket rule. If a revocation reached our database and its on-chain half failed, the cached path says revoked and the live path still says active, and nothing retries that half. So treat a revoked verdict from either path as revoked. Neither lies in the dangerous direction alone; only together do they cover both failure modes.
The anti-replay model, as far as this page goes
Cross-endpoint replay is prevented by construction, because the method and path are inside the signed binding.
Same-endpoint replay is time-boxed by a skew window defaulting to sixty seconds, with a replay cache on by default.
And the package states its own limit: a verifier needing strict one-time semantics is told to issue its own challenge and verify against that. If your decision is consequential, take that advice rather than the default.
What to actually do, in order
- Require the status reference. Reject an envelope without one instead of treating it as clean.
- Treat
revoked: nullas a failure, not as an absence of bad news. - Supply the status issuer key, or every status reference fails closed and you have built a system that rejects everything.
- Check
staleMsand decide what age you accept, rather than accepting any age. - Use the live path for anything consequential, and issue your own challenge if you need one-time semantics.

