Skip to content
WeZend
CPaaS

Guide: verify WeZend webhook signatures in Express (with code)

A complete, working Express endpoint that receives delivery webhooks and rejects anything that isn't genuinely from WeZend.

WWeZend TeamJuly 5, 2026 · 5 min read

WeZend's webhooks are per-message: you pass webhook_url on the send request itself (see Webhooks), and this is the receiving side of that.

The endpoint

const express = require("express");
const crypto = require("crypto");

const app = express();
app.use(express.raw({ type: "application/json" })); // need the raw body to verify

app.post("/hooks/wezend", (req, res) => {
  const signature = req.get("X-ZafeConnect-Signature");
  const timestamp = req.get("X-ZafeConnect-Timestamp");
  const rawBody = req.body.toString("utf8");

  const expected = crypto
    .createHmac("sha256", process.env.WEZEND_WEBHOOK_SECRET)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const isValid =
    signature &&
    expected.length === signature.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

  if (!isValid) return res.status(401).send("invalid signature");

  // Respond fast — do the real work after acknowledging.
  res.sendStatus(200);

  const payload = JSON.parse(rawBody);
  // e.g. payload.event === "message.delivered"
  processDeliveryEvent(payload).catch(console.error);
});

Three things worth getting right

  1. Use the raw body for the signature check, not the parsed/re-serialized JSON — re-serializing can subtly change whitespace or key order and break the HMAC comparison.
  2. Respond 2xx before doing slow work. A failed or slow response triggers a retry (30s, 5 minutes, 30 minutes) — that's fine for a real outage, wasteful for a webhook handler that's just slow.
  3. The header names still say X-ZafeConnect-*, not X-WeZend-* — a leftover from the platform's original name that hasn't been renamed in the backend yet. Match the header name exactly as shown above; don't assume it follows the current brand.

Where the signing secret comes from

Find (and rotate) it in Settings → Webhook secret, backed by GET /v1/webhook-secret and POST /v1/webhook-secret/rotate.

One platform. Every customer interaction.

Replace your patchwork of messaging APIs, CDP and automation tools with a single engagement platform built for scale.

No credit card required · EU data residency · 99.99% uptime SLA