Skip to main content

Signature Verification

If you configure a signing secret on your webhook endpoint, Waplify signs every payload using HMAC-SHA256 (a security algorithm that creates a unique fingerprint of the data using your secret key). This lets you verify that the request genuinely came from Waplify and wasn't tampered with.

How it works

  1. When you create a webhook endpoint, optionally set a secret string
  2. For every delivery, Waplify computes HMAC-SHA256(secret, request_body) and includes the result in the X-Webhook-Signature header
  3. Your server recomputes the same HMAC using the raw request body and your secret
  4. If the values match, the request is genuine

Signature header format

X-Webhook-Signature: sha256=a1b2c3d4e5f6...

The value after sha256= is the hex-encoded HMAC digest.

Verification examples

Python

import hmac
import hashlib

def verify_signature(body: str, secret: str, signature_header: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
body.encode("utf-8"),
hashlib.sha256
).hexdigest()
received = signature_header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)

# Usage in a Flask endpoint:
@app.route("/webhook", methods=["POST"])
def handle_webhook():
body = request.get_data(as_text=True)
signature = request.headers.get("X-Webhook-Signature", "")

if not verify_signature(body, "your_secret_here", signature):
return "Invalid signature", 401

data = request.get_json()
# Process the webhook event...
return "OK", 200

Node.js (Express)

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

// IMPORTANT: Use express.raw() for the webhook route to get the raw body.
// Do NOT use express.json() here — it parses the body, which breaks
// signature verification.
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf-8"); // Buffer → string
const signature = req.headers["x-webhook-signature"] || "";

// Verify the signature
const expected = crypto
.createHmac("sha256", "your_secret_here")
.update(rawBody, "utf-8")
.digest("hex");
const received = signature.replace("sha256=", "");

const isValid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(received)
);

if (!isValid) {
return res.status(401).send("Invalid signature");
}

const data = JSON.parse(rawBody);
// Process the webhook event...
res.status(200).send("OK");
});
Important

Always use constant-time comparison (like hmac.compare_digest in Python or crypto.timingSafeEqual in Node.js) to prevent timing attacks. Do not use == or === for signature comparison.

Best practices

  • Always verify signatures in production — don't skip this step
  • Use a strong secret — at least 32 random characters
  • Read the raw request body — verify the signature against the raw body string, not a parsed/re-serialized version
  • Respond quickly — return a 200 status within 5 seconds. Do heavy processing asynchronously after acknowledging the webhook
  • Handle duplicates — in rare cases, the same event may be delivered more than once. Design your handler to be safe against duplicates (e.g., check if you've already processed a message_id)