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 inX-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-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.signatureform - 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 JWKSkid 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 ofbase64url(header) + "." + base64url(payload).
5. Validate claims
Validation rules:
body_hash_algmust besha-256expmust equaliat + 900iatmust 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.
Replay prevention
JWT verification does not prevent replay attacks. Store each acceptedjti for at least 15 minutes (the signature lifetime) and reject any delivery whose jti you have already processed.