React to eventsVerify signatures

Verify webhook signatures

Copy-paste signature checks in Node, Python and PHP.

Every delivery is signed. Check the signature before you trust a request. The rules are in Verifying signatures; this guide gives working code.

The signed string is:

event_id + "." + timestamp + "." + raw_body

where event_id is the X-Imploy-Event-Id header, timestamp is X-Imploy-Timestamp, and raw_body is the body exactly as received. The HMAC-SHA256 of that string, as hex and prefixed with sha256=, must equal X-Imploy-Signature.

import crypto from "node:crypto";

export function isValid(req, rawBody, secret) {
  const eventId = req.header("X-Imploy-Event-Id");
  const timestamp = req.header("X-Imploy-Timestamp");
  const signature = req.header("X-Imploy-Signature") ?? "";

  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", secret)
      .update(`${eventId}.${timestamp}.`)
      .update(rawBody)
      .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Common mistakes

  • Parsing the body first. Re-encoding JSON changes the bytes. Verify against the raw body, then parse.
  • Wrong secret. Use the webhook's signing secret, not your access token or the optional secret header. You can reveal it on the webhook in Settings.
  • Normal comparison. Use a constant time compare, as above.
  • After rotating. Rotating changes the signing secret straight away. Update your endpoint at the same time.

Reject old requests

The timestamp is in the signed string, so it can't be changed without breaking the signature. Reject a delivery whose timestamp is more than a few minutes old, so a captured request can't be replayed later.

Return 401 for a bad signature and don't process the body.