signelloAPI
Webhooks

Verifying deliveries

Check the signature before you trust a request. Node, Python, PHP and C#.

Anyone can POST JSON at your endpoint. The signature is what tells you a delivery came from Signello and has not been altered or replayed.

The header

X-Signello-Signature: t=1788432862,v1=5f2c9ab4…
  • t is the Unix timestamp, in seconds, of when we signed the request.
  • v1 is a HMAC-SHA256, hex-encoded, of {t}.{raw body}, keyed with the endpoint's signing secret.

The timestamp is part of the signed material, not just a hint. Signing the body alone would let anyone who captured one delivery replay it forever.

The four steps

Read the raw body

Take the bytes exactly as they arrived. Parsing to an object and re-serialising changes key order and whitespace, and the signature will not match. In Express that means express.raw(), not express.json().

Rebuild the signed string

Concatenate the timestamp, a literal dot, and the raw body: ${t}.${body}.

Compare in constant time

Compute HMAC-SHA256 with your secret and compare against v1 using a timing-safe comparison. A plain == leaks how much of the signature you matched, one byte at a time.

Reject anything old

If t is more than five minutes from now, refuse the request even when the signature checks out. Without this the signature is valid forever and a captured delivery can be replayed at will.

Code

verify.js
import crypto from "node:crypto";

const TOLERANCE_SECONDS = 300;

export function verifySignelloSignature(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(",").map((piece) => piece.split("=", 2)));
  const timestamp = Number(parts.t);
  const signature = parts.v1;
  if (!timestamp || !signature) return false;

  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

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

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

const app = express();

app.post("/webhooks/signello", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifySignelloSignature(
    req.get("X-Signello-Signature") ?? "",
    req.body.toString("utf8"),
    process.env.SIGNELLO_WEBHOOK_SECRET
  );
  if (!ok) return res.status(400).send("bad signature");

  const event = JSON.parse(req.body.toString("utf8"));
  // Answer first, do the work afterwards.
  res.sendStatus(200);
  void handle(event, req.get("X-Signello-Delivery-Id"));
});

Testing it

Hit Test on the endpoint in Settings → Integrations → API & Webhook. The ping it sends is signed exactly like a real delivery, so if your verification passes on a ping it will pass on everything else.

If it never matches

On this page