Stridee
Stridee Docs

Verifying a delivery

Every delivery is signed with our Ed25519 key and verified against the JWK Set we publish — JWS detached, RFC 7515.

Anyone who learns your URL can POST to it, and anyone can seal a body to your public key. The webhook-signature header is what proves a delivery came from us: it is made with a key only we hold, over the exact bytes on the wire.

Our public keys

Text
https://api.stridee.com/.well-known/jwks.json

A standard JWK Set of Ed25519 keys, so any JOSE library handles fetching, caching and picking the key by kid:

What it serves
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "_1dXXcevi_xNCDfMHOIBe2hqiBRdxVealY40Yv6akI4",
      "use": "sig",
      "alg": "EdDSA",
      "kid": "whk_2026_08"
    }
  ]
}

Fetch it and cache it (Cache-Control says ten minutes). Never copy a key's x into your config — we rotate by publishing a second key, then signing with it, and a client that fetches the set needs no change when we do. A pinned key breaks.

The header

webhook-signature is a JWS with detached content (RFC 7515 Appendix F) — three segments with the middle one empty, because the payload is the request body you already have.

Its protected header, decoded
{
  "alg": "EdDSA",
  "kid": "whk_2026_08",
  "webhook-id": "c41e9b02-7a3d-4e58-8f19-6b0d2c85af73",
  "webhook-timestamp": 1770124811
}

Read the id and timestamp from here, not from the HTTP headers. They carry the same values, but only this copy is signed.

Verifying

Verify the raw body. A framework that parses JSON for you has already thrown away the signed bytes, and re-serializing them will not verify.

import { base64url, createRemoteJWKSet, flattenedVerify } from 'jose';

// Once, at startup.
const JWKS = createRemoteJWKSet(new URL('https://api.stridee.com/.well-known/jwks.json'));

export async function verifySignature(header, rawBody) {
  const [protectedSegment, , signature] = header.split('.');
  return flattenedVerify(
    { protected: protectedSegment, signature, payload: base64url.encode(rawBody) },
    JWKS,
    { algorithms: ['EdDSA'] }
  );
}

The body is base64url-encoded first because the JWS uses the default b64: true.

Freshness

A signature never expires, so reject old ones:

JavaScript
export function assertFresh(timestamp) {
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) throw new Error('outside the tolerance window');
}

Five minutes is a clock-skew allowance. It limits how long a captured delivery stays useful; deduping on webhook-id is what stops it being processed twice. You want both — the full order is on Webhooks.

If it doesn't verify

SymptomUsually
signature verification failedYou verified re-serialized JSON rather than the raw bytes, or the body was changed in transit.
No key matches the kidA stale cache or a pinned key — fetch the set.
The header is missingA proxy upstream stripped it.

Something wrong or missing on this page? Tell us in Discord. Need something the API doesn’t do yet? Request it on the roadmap.