Docs · Reference
Verifying a credential.
Anyone can verify. No wallet, no account, no auth header. Verification checks the signature locally, and (when reachable) asks the chain whether the credential is still fresh.
Two surfaces
- Interactive:
/poa/verify. Paste a JWS into the form, get a human-readable report. - Programmatic:
POST /poa/api/verify. Same checks, machine response.
What gets checked
Three independent axes:
- Signature. The JWS is verified against the issuer's public key (kid
theseus-poa-2026-04, served from/poa/.well-known/jwks.json). The algorithm is pinned toEdDSA. - Revocation list. The JTI is checked against the issuer's revocation list. Revocation can be operator-initiated or automatic (see the next page).
- Chain freshness. The current
AgentInfois fetched and compared against the snapshot in the credential. ABG drift, controller rotation, deregistration, or balance-zero-90d all trigger a stale verdict.
The verify endpoint
# Send the raw JWS as text/plain
curl -X POST https://theseus.network/poa/api/verify \
-H 'content-type: application/jose' \
--data 'eyJhbGciOiJFZERTQSI...'
# Or wrap it in JSON
curl -X POST https://theseus.network/poa/api/verify \
-H 'content-type: application/json' \
-d '{ "jws": "eyJhbGciOiJFZERTQSI..." }'Successful response
{
"valid": true,
"jti": "01HQXY...",
"agentId": "5GrwvaEF...HGKutQY",
"issuedAt": 1735689420123,
"issuer": "theseus.network/poa",
"kid": "theseus-poa-2026-04",
"claims": { /* the full PoACredentialClaims */ },
"bundles": {
"derived": true,
"list": [
{ "category": "DeFi", "name": "Trade", "intentTypes": ["..."] }
]
},
"freshness": { "status": "current" }
}Failure responses
// Bad signature, wrong alg, malformed JWS all collapse to:
{ "valid": false, "reason": "signature-invalid" }
// Revoked: signature is valid, but the credential is stale.
{ "valid": true, "freshness": { "status": "revoked", "reason": "abg-changed" }, /* ... */ }
// Chain unreachable: signature valid, freshness unknown.
{ "valid": true, "freshness": { "status": "unknown", "detail": "..." }, /* ... */ }Programmatic gating
When a downstream service uses the credential to make a trust decision (for example, “only let agents with a full KZG verification grade run this tool”), gate on the signed fields:
claims.sub. The agent ID this credential is for.claims.agent.abgHash/abgVersion. Pin to a specific configuration.claims.agent.capabilities.intentTypes. The raw permitted intents. Thebundlesfield on the response is a display-only grouping and is markedderived: true.claims.agent.recentRuns.grade. Refuseliteif you require integrity guarantees.
Verifying offline
For environments that cannot reach the verify endpoint, the JWS is a standard Ed25519 JWS. Any JOSE library can verify it given the public JWK. Fetch the JWK once from /poa/.well-known/jwks.json and cache it. You will lose the freshness check; pair offline verification with a periodic poll of /poa/api/revoked.
import { compactVerify, importJWK } from "jose";
const { keys } = await fetch("https://theseus.network/poa/.well-known/jwks.json")
.then((r) => r.json());
const key = await importJWK(keys[0], "EdDSA");
const { payload } = await compactVerify(jws, key, { algorithms: ["EdDSA"] });
const claims = JSON.parse(new TextDecoder().decode(payload));Rate limits
POST /poa/api/verify is capped at 60 requests per minute per IP. Comfortable for a developer iterating against the endpoint, tight enough to discourage casual scraping. Hitting the limit returns 429 with a Retry-After header.