Webhooks

Signature verification

Every delivery is signed with your webhook secret. Verify before you parse — an unsigned endpoint is an open door.

The scheme

algorithm
signature = HMAC_SHA256(secret, timestamp + "." + rawBody)
header    = X-JourneyStack-Signature: sha256=<hex>
  • secret is the whsec_… value shown when you save a webhook URL. Rotate it any time in the portal.
  • timestamp is the X-JourneyStack-Timestamp header, seconds since epoch.
  • rawBody is the exact bytes we sent. Verify before JSON parsing — re-serialising changes the bytes and breaks the signature.

Node example

js
import { createHmac, timingSafeEqual } from "crypto";

export function verify(rawBody, headers, secret) {
  const ts = headers["x-journeystack-timestamp"];
  const given = String(headers["x-journeystack-signature"] || "").replace("sha256=", "");
  if (!ts || !given) return false;

  // Reject anything older than five minutes to stop replays.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const a = Buffer.from(given, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Getting the raw body

js
// Express: mount a raw parser on the webhook path only
app.post("/hooks/journeystack", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body.toString("utf8"), req.headers, process.env.JS_WEBHOOK_SECRET)) {
    return res.status(401).send("bad signature");
  }
  const event = JSON.parse(req.body.toString("utf8"));
  enqueue(event);          // do the work off the request
  res.status(200).send("ok");
});

Respond fast, work later

Return 2xx within 10 seconds. Anything slower is treated as a failure and retried. Queue the payload and process it outside the request.

Rotating the secret

Rotating in the portal issues a new secret immediately; deliveries in flight were already signed with the old one. Accept both secrets for a few minutes during a rotation, then drop the old value.