Skip to main content
Zavu signs every webhook with HMAC-SHA256. Verify the signature before you trust a payload.

Signature Header

Every request carries X-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

Ignore parts you do not recognize, so a future scheme does not break your parser.

3. Check the timestamp

4. Recompute and compare

Use the t from the header, not your own clock.
Compare in constant time. === leaks how many characters matched.

Complete examples

Each one prefers v2 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.
Both signatures arrive in the same header, sharing one 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.
Going from v1 straight to v2 is rejected with 400. Step 2 is where you confirm your receiver works, and it is worth doing properly: a receiver that answers 200 before it verifies looks identical to a working one from our side, so a passing test request proves nothing. Watch your own logs.

Check the timestamp, and be idempotent

Two separate things, and you want both. Reject deliveries whose t 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:
  1. You are hashing the wrong payload. Check signatureVersion on the sender. On v1 hash the body alone; on v2 hash {t}.{body}.
  2. A body parser ran first. The signature covers the raw bytes. JSON.stringify(JSON.parse(x)) is not always x.
  3. Wrong secret. It is per sender. Regenerating it invalidates the old one immediately, with no overlap.
  4. Milliseconds. t is in seconds.

It worked, then stopped

Check whether the sender’s signatureVersion 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:
Expect 200. Then change one character of the body and expect 401. A receiver that returns 200 to both is not verifying anything.

Next Steps