Skip to main content
Every webhook has X-Tallwatch-Signature. Verify it before you process the body. Verification is how you know the POST came from someone who holds the channel secret, and that the bytes you received are the bytes Tallwatch sent. Skip it, and anyone who can reach the URL can POST a fake incident.opened. Check the timestamp as well, or a captured request can be replayed hours later.

What the header means

HMAC-SHA256 is a keyed hash: the same secret plus the same message always produce the same hex digest. Tallwatch computes that digest and puts it in the header. You compute it with the secret you stored as TALLWATCH_WEBHOOK_SECRET (the same value you pasted into the channel) and compare. The signed message is not the body alone. It is the timestamp, a dot, and the raw body, so a captured body cannot be paired with a fresh timestamp.
t is unix seconds (the same value as X-Tallwatch-Timestamp). v1 is the hex HMAC. Sign the raw bytes, not parsed JSON. JSON.parse then JSON.stringify can change key order and whitespace, which changes the HMAC. In Express, express.json() consumes the stream; use express.raw({ type: "application/json" }) (or the equivalent in your stack) so req.body is still a Buffer. Reject t more than 300 seconds from now (ahead or behind). That window covers clock skew. Outside it, treat the request as a replay. Compare with a constant-time check (crypto.timingSafeEqual in Node). A normal === can return faster when the first byte differs, which leaks how much of the expected hex an attacker guessed.
Return 401 and stop if the header is missing, v1 does not match, or t is stale. Do not parse JSON or open tickets first.

Node.js

This handler reads the raw body, recomputes v1, compares in constant time, then rejects a stale t. The HMAC input is t=${t}. as bytes concatenated with req.body, which is the same message as "t=" + t + "." + raw_body.
Node.js
timingSafeEqual throws if the buffers differ in length, so the example checks a.length !== b.length first. After both checks pass, parse the JSON and return 2xx within 10 seconds.

Rotate the secret

To rotate the secret, accept both old and new in your receiver, update the channel, then drop the old one.
  1. Generate a new secret (openssl rand -base64 32).
  2. In the receiver, verify against the new secret, and if that fails, against the old one (constant-time compare for each).
  3. Paste the new secret into the webhook channel and Send test.
  4. When test (and a real event, if you can wait) succeed with only the new secret needed, remove the old secret from the receiver.
If you change the channel first and the receiver still has only the old secret, every delivery 401s until you deploy. See for the body you parse after verification, and for the endpoint checklist.