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_secretonce. Store it securely; list/get never return it again. - Rotate:
POST …/webhooks/:id/rotate-secretreturns 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).
Signature header
Request header
X-Engagive-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>Signed string is {t}.{raw_body} where raw_body is the exact HTTP body bytes Engagive sent. Reject if clock skew is greater than 5 minutes.
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.