Signature Header
Every request carriesX-Zavu-Signature:
A header carries
v1, v2, or both. Which one you get is per receiver, and
you control it.
The two schemes
v2 is the current scheme, and it is what new webhooks use. It signs the
timestamp together with the body, so more of the request is covered by the
signature.
v1 signs the body only. Webhooks created before the scheme was
configurable are on v1 and stay there until you move them. There is no
deadline.
Moving takes three steps and no downtime. See
Migrating to v2 signatures.
Older versions of this guide described verifying
v1 by hashing
{timestamp}.{body}. That was wrong: v1 covers the body alone, so a receiver
built that way rejects every delivery. If that is what you have, either fix the
computation or move the sender to v2, where hashing {timestamp}.{body} is
correct.Which scheme is my webhook on?
Verifying
Four steps, in this order.1. Read the raw body
The signature covers the exact bytes that were sent. A parsed and re-serialized object is not those bytes.2. Parse the header
3. Check the timestamp
4. Recompute and compare
Use thet from the header, not your own clock.
=== leaks how many characters matched.
Complete examples
Each one prefersv2 and falls back to v1, so the same code works before,
during and after a migration.
Moving a webhook to v2
Three steps, and the middle one is what makes this safe. 1. Turn on both signatures.t. Your current
receiver still verifies v1 and notices nothing.
2. Deploy a receiver that verifies v2, and watch real deliveries.
The examples above already do this: they prefer v2 when it is present. Confirm
in your own logs that deliveries are landing before moving on.
3. Turn v1 off.
Check the timestamp, and be idempotent
Two separate things, and you want both. Reject deliveries whoset is far from now, as the examples above do. And
handle repeats: legitimate retries are real deliveries with a fresh
timestamp and a valid signature. Zavu retries non-2xx responses with backoff,
and delivery is at-least-once, so you will see the same event twice.
Store event.id and ignore what you have already processed.
Troubleshooting
Every delivery returns 401
In order of likelihood:- You are hashing the wrong payload. Check
signatureVersionon the sender. Onv1hash the body alone; onv2hash{t}.{body}. - A body parser ran first. The signature covers the raw bytes.
JSON.stringify(JSON.parse(x))is not alwaysx. - Wrong secret. It is per sender. Regenerating it invalidates the old one immediately, with no overlap.
- Milliseconds.
tis in seconds.
It worked, then stopped
Check whether the sender’ssignatureVersion changed, and whether the secret
was regenerated. Both take effect on the next delivery.
Testing locally
Send yourself a signed request rather than disabling verification:200. Then change one character of the body and expect 401. A
receiver that returns 200 to both is not verifying anything.
Next Steps
- Event Types - Understand webhook payloads
- Webhooks - Configure your endpoints
