Signing keys and rotation
What is in the public key set today, how rotation works, and what your verifier must do when it meets a kid it does not know.
Verdicts and webhook events are signed with ES256 (ECDSA on P-256). The public keys are published as a JWKS, and that document is the only thing a counterparty needs to check a signature.
https://verdicts.saifuro.com/.well-known/jwks.jsonThe set is served with Access-Control-Allow-Origin: * and Cache-Control: max-age=300, so a browser can fetch it and a five-minute cache is the intended behaviour, not an accident.
Two keys live in the set at once
The set holds one key per environment, both present simultaneously:
kid | Signs |
|---|---|
sandbox-2026-09 | every verdict from a sandbox |
verdict-2026-09 | every verdict from production |
They are published together on purpose. A counterparty integrating against a sandbox and a production operator at the same time resolves both from one document, and telling them apart is a prefix comparison on kid.
A kid beginning sandbox- must never be accepted as a production approval. This is the single most important rule on this page. Nothing else in the token distinguishes environments — the issuer, the claim set and the shape are identical.
The canonical URL, and the one that redirects
The URL above is canonical and is also the iss claim inside every verdict. An older location on the apex domain still answers and permanently redirects here.
That redirect is never removed. A verifier deployed against the old URL keeps working, and a rule that only ever adds locations is one nobody has to be notified about.
Rotation
Rotation is additive and overlapping, which is the opposite of how API keys rotate:
- The new key is published into the set alongside the current one.
- Signing switches to the new
kid. - The old key stays in the set for at least the cache lifetime, so tokens already in flight and verifiers holding a cached copy keep validating.
- Only then is the old key removed.
The overlap is never shorter than the 300-second cache. In practice it is much longer, because there is no cost to leaving a public key in a document.
What your verifier must do with an unknown kid
Fetch the set again, once, and re-check. A kid that is absent from a freshly fetched set is a token you must reject.
Do not pin a single key. Use a library that resolves by kid from the live set and caches it — createRemoteJWKSet in jose, a PyJWKClient in PyJWT, and their equivalents elsewhere. A hard-coded public key is an integration that breaks silently at the next rotation, and it breaks by rejecting valid approvals during a payment.
Do not retry in a loop. One refetch on an unknown kid, with your own backoff, is the correct behaviour; a verifier that hammers the set on every unknown token turns a rotation into an outage.
What this page does not cover
How the private keys are held is not described here. When there is an attestation worth reading rather than a paragraph asking to be believed, it will appear on security with the other certifications, at its real status.

