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.json

The 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:

kidSigns
sandbox-2026-09every verdict from a sandbox
verdict-2026-09every 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:

new key publishedold key removedverdict-2026-09verdict-2027-03both in the setsigning switches to the new kid as soon as it is publishedoverlap never shorter than the 300 s cache
  1. The new key is published into the set alongside the current one.
  2. Signing switches to the new kid.
  3. 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.
  4. 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.

On this page