Stridee
Stridee Platform 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

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

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.

Text
https://api.stridee.com/.well-known/jwks.json
What it serves
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "_1dXXcevi_xNCDfMHOIBe2hqiBRdxVealY40Yv6akI4",
      "use": "sig",
      "alg": "EdDSA",
      "kid": "whk_2026_08"
    }
  ]
}

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.

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

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

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.

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

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'] }
  );
}

Freshness

A signature never expires, so reject old ones:

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.

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

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.