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
- When you create a webhook endpoint, optionally set a secret string
- For every delivery, Waplify computes
HMAC-SHA256(secret, request_body)and includes the result in theX-Webhook-Signatureheader - Your server recomputes the same HMAC using the raw request body and your secret
- 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
200status 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)