API & Automation·6 min read·

Signing Webhook Deliveries So Clients Can Verify Them

Cryptographically sign webhook payloads with HMAC and give clients a safe verification path to reject forged or replayed deliveries quickly.

NB

Netbay Developer Relations

Netbay Engineering

On this page

Webhooks are how your API tells other systems something happened without them polling. The trouble is that the endpoint receiving them is usually public — anyone who knows the URL can fire a fake event. The fix is a signature the client can verify. This post shows how to sign webhook payloads with HMAC and how clients should verify them safely.

Why sign, not just authenticate

The receiving endpoint cannot easily authenticate the sender and still accept traffic from anywhere. A signature solves this: you share a secret with the client, HMAC the exact payload, and ship the digest in a header. The client recomputes it and compares. Because the secret is never transmitted, a false sender cannot forge valid signatures — and tampered payloads produce a different digest.

The critical detail is that the signature must cover the exact bytes the client received. If you sign only part of the payload, an attacker can alter the unsigned portion and the integrity check passes.

That byte-for-byte requirement is why you must decide, and document, how the signature string is composed. Some providers sign only the raw body; others prefix a timestamp or the method and path. Whatever you choose, ship the exact framing to consumers in your documentation and keep it stable across versions, because a client that reconstructs a slightly different byte string — re-encoding JSON with different whitespace, for instance — will compute a different digest and reject legitimate deliveries. If you ever reorder or reformat the payload, treat that as a versioned change to the signing contract, not a cosmetic edit.

sender HMAC(payload, secret) receiver recompute + compare payload + X-Signature secret shared out-of-band

Signing on the send side

Use HMAC-SHA256, and include a timestamp in the signed material so you can bound how old a delivery is (anti-replay). Keep the secret long and random, and rotate it on a schedule. Prefer a header that names the version so clients can migrate signatures gracefully.

The signing step, in Python

python
import hashlib, hmac, time, os

SECRET = os.environ["WEBHOOK_SECRET"]

def sign_payload(body):
    ts = str(int(time.time() * 1000))
    msg = ts.encode() + b"." + body  # exact bytes
    sig = hmac.new(
        SECRET.encode(), msg, hashlib.sha256
    ).hexdigest()
    return ts, sig

# on delivery:
# headers["X-Webhook-Signature"] = "v1=" + sig
# headers["X-Webhook-Timestamp"] = ts

Verifying as a receiver

The receiver must use a **constant-time comparison** to avoid timing leaks, verify the timestamp is recent to stop replay, and recompute over the raw body. A common failure is framing a JSON body with pretty-printing and then hashing one framing while the client hashes another — always hash the exact raw request body bytes.

javascript
const crypto = require("crypto");

function verifyWebhook(body, signatureHeader, tsHeader, secret, nowMs) {
  const maxAgeMs = 5 * 60 * 1000;
  if (Math.abs(nowMs - Number(tsHeader)) > maxAgeMs) {
    return false;
  }
  const msg = tsHeader + "." + body;
  const expected = crypto.createHmac("sha256", secret)
    .update(msg).digest("hex");
  const provided = signatureHeader.replace(/^v1=/, "");
  const a = Buffer.from(expected);
  const b = Buffer.from(provided);
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

What happens on a failed verification matters as much as the check itself. Fail closed: reject the delivery and surface the mismatch to your operators rather than silently dropping it, and consider whether to retry or to alert after a threshold of failures, since a burst of bad signatures is often the first sign of a misconfigured client or an active forgery attempt. Keep the failure detail in logs — the dominant sender, the signature version, the age of the timestamp — but never log full payloads or secrets.

Handle the edge cases

  • **Replays**: a captured, validly signed delivery can be replayed later. A fresh timestamp plus a short acceptance window is your defense, with optional idempotency keys from the sender.
  • **Retries**: duplicate deliveries are normal. Combine the signature check with a deduplication key in the event body.
  • **Key rotation**: keep two active secrets during rotation and accept a signature if either valid — prefer matching the version prefix.

Takeaway

Signature verification turns an open webhook URL into a tamper-proof, replay-resistant channel. Sign the exact payload bytes, add a timestamp, compare in constant time, and rotate keys. You can wire this pattern into app code running on a Netbay VPS, from DC01 in Lucknow — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance