Bad signature, key resolution failed, or Content-Digest ≠ the body bytes
Sign and send the exact body bytes; don’t let your transport re-serialize JSON after signing
expired_signature
now is past expires + skew — almost always clock drift
Run NTP. Signatures cap at 300 s and tolerate only ±60 s of skew
replayed_nonce
This (keyid, nonce) was already seen — or your verifier’s nonce store isn’t shared across instances
Agents: fresh random nonce per request (the SDK does this). Services: share one nonce store across all instances (§5.6)
revoked_key
The signing DID is on the service’s revocation list
The key was revoked. Re-key through the owner and re-link; don’t retry
Clock skew is the most common first-request failure. A signed request is valid for at most 300 seconds and tolerates ±60 s of drift between agent and service. If your agent’s clock is off — common in containers and CI — every signature reads as future-dated or expired. Sync time before debugging anything else.
replayed_nonce under load on a multi-instance service usually means a per-process nonce cache. §5.6 requires the seen-(keyid, nonce) set to be shared and atomic across every verifier instance. On Cloudflare, use DurableObjectNonceStore, not KvNonceStore, for atomic check-and-set. See Deploy to Cloudflare Workers.
The service is attested_only and no valid attestation was present — at signup, or an attested session’s window lapsed (§10.7)
Link to a human once, then mint a per-service JWT. The SDK’s AttestedFetcher and afauth call re-mint on this challenge automatically
invalid_attestation
JWT signature failed, iss ≠ afauth-trust, aud ≠ your service_did, sub ≠ the signing DID, or exp is past
Mint a fresh JWT (≤ 900 s lived). Check your service_did matches the aud claim byte-for-byte
A common invalid_attestation cause is an aud mismatch: the agent minted for did:web:api.example.com but the service’s service_did is did:web:example.com. They must be identical.
Revocation has two levers, each with a deliberate latency (Revocation):
Local revoke at a service is immediate at that service but propagates nowhere else — there’s no global revocation bus (§8.3).
Global revoke at the attestor stops new attestations, but tokens already minted stay valid until they expire (≤ 15 min). Services that only checked attestation at signup never see it — unless they run attested sessions, which close the gap within the freshness window.
To cut a stolen key off everywhere, act at each service and the attestor. The full runbook is Recover a compromised key.