WaConnect

Receive messagesVerify webhooks

Verify webhooks

Check that a webhook really came from WaConnect.

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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t — when we sent it (Unix seconds).
  • v1 — an HMAC-SHA256 of t, a dot, and the raw request body, keyed with your signing secret, in hex.

How to verify#

  1. Read the raw request body — before any JSON parsing, which can change spacing.
  2. Compute HMAC-SHA256(secret, t + "." + rawBody) as hex.
  3. Compare it to v1 with a constant-time comparison.
  4. Reject requests whose t is 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#

HeaderValue
Content-Typeapplication/json
User-AgentWaConnect-Webhooks/1.0
WaConnect-Event-IdThe 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.