Signed Badges and Verification
A badge image is a claim; anyone can screenshot a green "passing" badge. Signed metadata turns the claim into a receipt: the service signs the template identity, version, resolved-state hash, and a validity window with its Ed25519 key, and anyone - including offline consumers - can verify it.
Getting signed metadata
Two switches, either is enough: set
"verification": {"signed_metadata": true} on the template so every JSON
render carries metadata, or pass "include_metadata": true on a single
POST /api/v1/badges/render request. The image GET routes return only
the SVG (with the state hash as the ETag) - to obtain the receipt, use
the JSON render. The response's verification block, captured live:
{
"template_id": "XHSbLagCVnnPAZvejzAu",
"template_version": 1,
"state_hash": "sha256:ca0aeea44cb44f964b81a0d3f5be6f26661bcbdb294091fb2198a5ce17b352ad",
"issued_at": "2026-08-28T13:29:10.969519+00:00",
"expires_at": "2026-08-28T13:29:40.969519+00:00",
"signature_alg": "ed25519",
"signature": "Vqc99EBUeL7IDa1Ft1U4doWiL6ODBLf6DgHMppbedscvRTtAgwKZb8o7Ss40J9BPdS3fQXjR3rPJ8sSP8_wyCg",
"public_key": "3zP35w_-lNmXazf64MV4bvzeV3BXbM1ovGw8-yl2rUM"
}The validity window equals the template's cache TTL (30 seconds here): a receipt is only as fresh as the render it describes.
Verifying
POST /api/v1/badges/verify takes the metadata block as its request body;
the CLI reads it from a file:
snug badge verify --metadata metadata.json{ "valid": true, "template_id": "XHSbLagCVnnPAZvejzAu", "template_version": 1 }Verification is fail-closed and always answers 200 with a valid flag -
branch on valid, not on the HTTP status. Both failure modes:
{ "valid": false, "reason": "signature mismatch", "...": "..." }
{ "valid": false, "reason": "signature has expired", "...": "..." }The first came from altering state_hash after signing; the second from
re-verifying the same metadata after its expires_at passed.
Offline and external verification
The signing public key is available to any authenticated caller:
snug badge public-key
# Algorithm: ed25519
# Public Key: 3zP35w_-lNmXazf64MV4bvzeV3BXbM1ovGw8-yl2rUMIt is also embedded in every metadata block, so a consumer can pin it once
and verify signatures without trusting the server that served the badge.
When a VerifyRequest includes a public_key (base64url), the service
verifies against that key instead of its own, by replaying
a fresh metadata block with its public_key field intact, which exercises
the caller-supplied-key path. Verification failures are also counted per
template in snug badge template-metrics as verification_failures.
The exact request and response fields are in the Badge API reference.