Signatures
Verify X-Evoriqa-Signature as an HMAC-SHA256 over the raw request body, with constant-time comparison — in Node and Python — and rotate a secret over the API.
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 (or last rotated its secret). Each feature group's endpoint has its own secret — verify with the secret of the group the event belongs to.
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 (one per feature group). Rotate it in place:
curl -X PUT "https://app.evoriqa.com/api/v1/webhooks/leads" \
-H "x-api-key: $EVORIQA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "rotateSecret": true }'The response carries the new secret — once, never again. Deliveries sign with the endpoint's current secret at send time, so deploy the new secret to your handler promptly: anything delivered after the rotation verifies only against the new value. If you cannot deploy instantly, accept either secret for the switchover window.
Where to go next
Last updated