Signatures
Verify X-Evoriqa-Signature as an HMAC-SHA256 over the raw request body, with constant-time comparison — in Node and Python.
Your endpoint URL is reachable by anyone who learns it. The signature is what tells a genuine delivery from a forged one, so verify every request before acting on it.
How the signature is computed
X-Evoriqa-Signature = hex( HMAC-SHA256( endpoint secret, raw request body ) )The secret is the whsec_… value shown once when you created the endpoint.
Verify against the raw bytes of the request body, exactly as received. If your framework parses the JSON and you re-serialise it to verify, key order and whitespace can differ from what was signed and every verification fails — or, worse, appears to work in testing and fails on one unusual payload. Capture the raw body before parsing.
Compare with a constant-time comparison. A plain === leaks timing information that can be used to forge a signature byte by byte.
Verifying
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.EVORIQA_WEBHOOK_SECRET;
// express.raw keeps the exact bytes — express.json() would destroy them.
app.post(
"/webhooks/evoriqa",
express.raw({ type: "application/json" }),
(req, res) => {
const received = req.get("X-Evoriqa-Signature") ?? "";
const expected = crypto
.createHmac("sha256", SECRET)
.update(req.body) // Buffer of the raw body
.digest("hex");
const a = Buffer.from(received, "utf8");
const b = Buffer.from(expected, "utf8");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("bad signature");
}
const event = JSON.parse(req.body.toString("utf8"));
res.status(200).end(); // acknowledge first
handle(event); // work afterwards
},
);import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["EVORIQA_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/evoriqa")
def evoriqa_webhook():
raw = request.get_data() # raw bytes, before any parsing
received = request.headers.get("X-Evoriqa-Signature", "")
expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(received, expected):
abort(401)
event = request.get_json()
handle(event) # keep this fast, or queue it
return "", 200A checklist
- [ ] Read the raw body before any JSON parsing.
- [ ] Recompute the HMAC with the endpoint's own secret.
- [ ] Compare in constant time.
- [ ] Reject with
401on a mismatch — do not process the payload. - [ ] Only then parse and act.
Rotating a secret
A secret is per endpoint. To rotate, register a new endpoint with the new secret, run both for as long as it takes to deploy the change, then delete the old one. Deliveries always use the endpoint's current secret at send time.
Where to go next
Last updated