Channels · WhatsApp · Webhooks

Webhook signing

Every WhatsApp webhook URL gets a webhook_secret returned once on create (or rotate). Engagive signs each POST with HMAC-SHA256 so you can reject forged deliveries.

Secret lifecycle

  • Create webhook → response includes webhook_secret once. Store it securely; list/get never return it again.
  • Rotate: POST …/webhooks/:id/rotate-secret returns a new secret once and invalidates the previous one.
  • Existing URLs created before signing: rotate once to enable verification (unsigned POSTs until a secret exists).

Node.js verifier

verifyEngagiveSignature
import crypto from "node:crypto";

const MAX_SKEW_SECONDS = 5 * 60;

export function verifyEngagiveSignature({ secret, rawBody, headerValue, nowSeconds = Math.floor(Date.now() / 1000) }) {
  const parts = Object.fromEntries(
    String(headerValue || "")
      .split(",")
      .map((p) => p.trim().split("="))
      .filter((kv) => kv.length === 2)
      .map(([k, v]) => [k, v]),
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!Number.isFinite(t) || !v1) return false;
  if (Math.abs(nowSeconds - t) > MAX_SKEW_SECONDS) return false;

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

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

// Express example — use the raw body string, not re-stringified JSON:
// app.post("/hooks", express.raw({ type: "application/json" }), (req, res) => {
//   const rawBody = req.body.toString("utf8");
//   const ok = verifyEngagiveSignature({
//     secret: process.env.ENGAGIVE_WEBHOOK_SECRET,
//     rawBody,
//     headerValue: req.get("X-Engagive-Signature"),
//   });
//   if (!ok) return res.status(401).send("invalid signature");
//   const event = JSON.parse(rawBody);
//   res.sendStatus(200);
// });

Retries

Failed deliveries (timeout / non-2xx) are queued and retried with exponential backoff (default 4 attempts). Idempotent handlers should key on event_id.

Webhook payload examples