Verifying tokens independently
An Authorize token is a signed proof that a human authorized an action. Since it's signed with ES256 (ECDSA P-256) and the public key is published as a JWKS, any relying party, such as a bank, a merchant or another agent, can verify it without an API key. The proof stands on its own.
The JWKS endpoint
GET https://<your-api-host>/.well-known/jwks.json
Public, unauthenticated, cacheable (Cache-Control: max-age=3600). It returns the current signing key (and, during a rotation, the previous key) so tokens keep verifying across a key change:
{ "keys": [ { "kty": "EC", "crv": "P-256", "x": "…", "y": "…", "kid": "…", "use": "sig", "alg": "ES256" } ] }
Each token's JWS header carries the kid that identifies which key signed it.
Verify our work
The same published keys sign every intent token, decision receipt and trace manifest, and evidence bundles carry their own offline verifier. You can check any of them without an API key and without trusting us. Paste one into the verify page to try it, or use the SDK below.
What offline verify proves — and what it does NOT
Offline verification (against the JWKS) proves authenticity and expiry: the token was really signed by this issuer and hasn't expired. It does not check:
- Action / payload binding — that the
action_description(and, when present, the exactaction_payload) match what was authorized. That needs the facts, so it's done byPOST /v1/intent/verify. - Revocation — a token can be revoked or killed before it expires. Offline verifiers can't see that.
For high-value actions, call POST /v1/intent/verify (the full check) or at minimum GET /v1/intent/status/:jti to check revocation. Offline verify is for cheap, low-stakes gating and for parties who cannot call the API.
Verify with the SDK
import { fetchJwks, verifyIntentTokenOffline } from "@provenance/sdk";
const jwks = await fetchJwks("https://your-api-host");
const result = await verifyIntentTokenOffline(token, jwks);
// { valid, expired, payload, reason? } — authenticity + expiry only
if (result.valid) {
console.log("authorized by", result.payload.sub, "session", result.payload.session);
}
fetchJwks and verifyIntentTokenOffline use WebCrypto, so they run unchanged in Node 18+ and the browser — no API key, no network to our verify endpoint.
Verify with any standard JWT library
The token is a standard ES256 JWS, so any JOSE library works. For example, with jose:
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(new URL("https://your-api-host/.well-known/jwks.json"));
const { payload } = await jwtVerify(token, JWKS, { algorithms: ["ES256"] });
// payload.jti, payload.sub, payload.session, payload.action_hash, …
No-code verification
Not ready to write code? Paste any token into the public verify page — it
runs the exact ES256 + JWKS check above in your browser, adds a keyless revocation
check, and shows the claims. See the verify-page guide. The
same browser-safe helpers ship in @provenance/sdk (verifyIntentTokenOffline,
verifyAttestedImageOffline).
Key rotation
To rotate the signing key, set the new key as INTENT_SIGNING_KEY_PEM and move the old one to INTENT_SIGNING_KEY_PEM_PREVIOUS. Both appear in the JWKS, so tokens signed by the old key keep verifying until they expire (the 4-hour TTL bounds the window). After that window, drop the previous key.