Why verify
Your webhook URL is effectively public — anyone who guesses it or spots it in a config file can sendPOST requests to it. Signature verification
proves that a given request actually came from Quippy and wasn’t modified
in flight.
Headers on every delivery
| Header | Example | Purpose |
|---|---|---|
X-Quippy-Event | exam.completed | Which event type this is. |
X-Quippy-Delivery-Id | dlv_01abc… | Stable ID for this delivery. Log it — it lines up with the admin portal’s deliveries view. |
X-Quippy-Signature | sha256=<hex> | HMAC-SHA256 of the raw request body, prefixed with sha256=. |
Content-Type | application/json | Always JSON. |
The algorithm
Read the raw bytes of the body
Do not parse the JSON and re-stringify it. The signature is computed
over the exact bytes Quippy sent. Any re-serialization (key reordering,
whitespace changes) will break verification.
Compute HMAC-SHA256(body, signing_secret)
Use your endpoint’s signing secret (the
whsec_… string from endpoint
creation) as the key. Hex-encode the result (lowercase).Paste-and-run samples
Reproduce a signature locally
Given the same body + secret, you should be able to reproduce the signature on your machine. Useful when debugging a mismatch:X-Quippy-Signature header on a delivery with
the same body. If it doesn’t:
- You’re hashing a modified body (pretty-printed, re-serialized, BOM added).
- Your env var isn’t exactly the secret — no extra whitespace, no quotes.
- You grabbed the wrong endpoint’s secret.
Common pitfalls
Parsing JSON before hashing
Parsing JSON before hashing
The JSON parsers in most languages drop whitespace and may reorder keys.
Always hash before parsing. In Express use
express.raw(...); in
Flask use request.get_data(); in Go read r.Body directly.Using == or === to compare
Using == or === to compare
String equality is timing-leaky. Use
crypto.timingSafeEqual (Node),
hmac.compare_digest (Python), hash_equals (PHP), hmac.Equal (Go),
or Rack::Utils.secure_compare (Ruby).Slow endpoint → repeated retries
Slow endpoint → repeated retries
The dispatcher times out at 10 seconds. If your endpoint does heavy
work synchronously, it will exceed the timeout, Quippy will retry, and
you’ll end up processing the same event multiple times. Respond fast;
do real work async.
Dedupe on X-Quippy-Delivery-Id
Dedupe on X-Quippy-Delivery-Id
Retries reuse the same
delivery_id. Persist the ID and short-circuit
duplicates so a retried delivery doesn’t double-count in your system.