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:
GET https://api.waterfall.io/.well-known/jwks.json
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:
FieldRequired value
algEdDSA
typJWT
kidnon-empty string

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

ClaimTypeDescription
iatintegerIssued-at timestamp (Unix seconds)
expintegerExpiry. Always iat + 900 (15 minutes).
jtistringUnique delivery ID. Use for replay deduplication.
job_idstringThe Waterfall job this delivery belongs to.
body_hashstringUnpadded base64url SHA-256 of the raw request body.
body_hash_algstringAlways sha-256.
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

CodeCause
missing_signature_headerX-Webhook-Signature is absent
signature_header_too_largeHeader exceeds 8192 bytes
malformed_compact_jwtJWT does not have three dot-separated segments
malformed_jwt_segmentA segment failed base64url decoding
invalid_algalg is not EdDSA
invalid_typtyp is not JWT
missing_kidkid is empty or absent
jwks_fetch_failedJWKS endpoint returned an error
unknown_kidNo JWKS key matches the JWT kid
invalid_signatureEd25519 signature verification failed
invalid_time_claimsiat or exp is missing or malformed
invalid_jtijti is missing or malformed
invalid_job_idjob_id is missing or malformed
invalid_body_hashbody_hash is missing or malformed
unsupported_body_hash_algbody_hash_alg is not sha-256
invalid_expiry_windowexp does not equal iat + 900
issued_in_futureiat is more than 300 seconds ahead of server clock
expired_signatureCurrent time is past exp
body_hash_mismatchComputed body hash does not match body_hash

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