Every webhook WaConnect sends is signed with your endpoint's signing secret, so your server can prove two things before acting on it: the request came from WaConnect, and nobody changed it on the way.
The signature header#
WaConnect-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt— when we sent it (Unix seconds).v1— an HMAC-SHA256 oft, a dot, and the raw request body, keyed with your signing secret, in hex.
How to verify#
- Read the raw request body — before any JSON parsing, which can change spacing.
- Compute
HMAC-SHA256(secret, t + "." + rawBody)as hex. - Compare it to
v1with a constant-time comparison. - Reject requests whose
tis more than 5 minutes old — this stops an old request being replayed later.
import { createHmac, timingSafeEqual } from "node:crypto"
export function isGenuineWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")))
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex")
return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
// Express: app.post("/webhooks/waconnect", express.raw({ type: "application/json" }), …)
// isGenuineWebhook(req.body.toString("utf8"), req.get("WaConnect-Signature"), process.env.WACONNECT_WEBHOOK_SECRET)If verification fails, answer 401 and ignore the request.
Other headers#
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | WaConnect-Webhooks/1.0 |
WaConnect-Event-Id | The event's id — the same on every retry, handy for skipping duplicates. |
Rolling the secret#
In Developer settings, open the webhook's menu → Signing secret → Roll secret. The old secret stops working at once, so update your server with the new one straight away.