Skip to main content
Webhook signing applies to every endpoint that accepts webhook_url. Each signed delivery includes an X-Webhook-Signature header containing a compact JWT. Your server uses this to confirm the delivery came from Waterfall and that the body was not modified in transit.If signing configuration is unavailable, Waterfall delivers the webhook without the X-Webhook-Signature header to preserve backward compatibility.

How it works

Waterfall signs each webhook delivery with an Ed25519 private key and puts the result in X-Webhook-Signature as a compact JWT. The body payload is not modified. The JWT contains a body_hash claim: the unpadded base64url SHA-256 digest of the exact raw request body bytes. Verification has three parts: valid signature, valid claims, matching body hash.

JWKS endpoint

Waterfall publishes its Ed25519 public keys at:
Cache the response for 5 minutes (Cache-Control: public, max-age=300). During key rotation, old and new keys overlap for at least the 5-minute cache window so in-flight deliveries continue to verify. See the Webhook JWKS reference for the full response schema.

Verification steps

Follow these steps in order. Reject the request on any failure.

1. Read the signature header

  • Header name: X-Webhook-Signature
  • Header value: one compact JWT in header.payload.signature form
  • Reject if absent or larger than 8192 bytes

2. Decode the JWT

Split on .. Base64url-decode each of the three segments (no padding). The protected header must contain:

3. Fetch and cache public keys

Fetch the JWKS and select the key where the JWKS kid matches the JWT kid. JWK shape: kty: "OKP", crv: "Ed25519", public key bytes in x. If the kid is not found in a cached response, refresh the JWKS once before rejecting. Throttle forced refreshes to one per 30 seconds.

4. Verify the signature

Verify the Ed25519 signature over the ASCII bytes of base64url(header) + "." + base64url(payload).

5. Validate claims

Validation rules:
  • body_hash_alg must be sha-256
  • exp must equal iat + 900
  • iat must not be more than 300 seconds ahead of your server clock
  • Current time must be before exp

6. Hash the body and compare

Hash the raw request body bytes before any JSON parsing. Do not reserialize. body_hash is the unpadded base64url SHA-256 digest of those exact bytes.
If the signature is valid but the body hash does not match, reject the request. A mismatch means the body was modified in transit.

Replay prevention

JWT verification does not prevent replay attacks. Store each accepted jti for at least 15 minutes (the signature lifetime) and reject any delivery whose jti you have already processed.

Error codes

Verification kit

Waterfall provides a reference implementation with a single-file verifier for Python and TypeScript, designed to be copied directly into your app. webhook-verification-kit on GitHub