Every FilingIQ webhook request carries a FilingIQ-Signature
header. Verify it before trusting the payload so a tampered or replayed
request can never push false signals into your systems.
FilingIQ-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256> v1 is HMAC-SHA256 of ${timestamp}.${raw_body}
using the signing secret returned at endpoint creation. The 5-minute
timestamp tolerance defends against replay attacks even when the HMAC
is valid.
Verification is a dozen lines of the standard-library node:crypto
module, no dependency required. Use raw-body parsing in Express so the
body bytes match exactly what FilingIQ signed.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
function verifyWebhookSignature(rawBody, secret, header, toleranceSec = 300) {
if (!header) return false;
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=")),
);
const ts = Number(parts.t);
if (!Number.isFinite(ts) || ts <= 0) return false;
if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false; // replay window
const expected = createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
const v1 = parts.v1 ?? "";
if (v1.length !== expected.length) return false;
return timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"));
}
app.post(
"/filingiq-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.header("FilingIQ-Signature");
const raw = req.body.toString("utf8");
if (!verifyWebhookSignature(raw, process.env.FILINGIQ_WEBHOOK_SECRET, sig)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(raw);
console.log(
event.event_type,
event.ticker,
event.signal_score,
`${event.latency_ms}ms after SEC`,
);
res.status(200).send("ok");
},
);
app.listen(3000); Standard-library Python plus Flask. No third-party crypto dependency.
import hmac
import hashlib
import os
import json
import time
from flask import Flask, request
app = Flask(__name__)
def verify(raw_body: bytes, secret: str, header: str, tolerance_sec: int = 300) -> bool:
if not header:
return False
parts = dict(
part.split("=", 1)
for part in (p.strip() for p in header.split(","))
if "=" in part
)
try:
ts = int(parts.get("t", "0"))
except ValueError:
return False
if ts <= 0:
return False
if abs(time.time() - ts) > tolerance_sec:
return False
expected = hmac.new(
secret.encode("utf-8"),
f"{ts}.{raw_body.decode('utf-8')}".encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
@app.post("/filingiq-webhook")
def webhook():
secret = os.environ["FILINGIQ_WEBHOOK_SECRET"]
if not verify(request.data, secret, request.headers.get("FilingIQ-Signature", "")):
return "invalid signature", 401
event = json.loads(request.data)
print(
event["event_type"],
event["ticker"],
event["signal_score"],
f"{event['latency_ms']}ms after SEC",
)
return "ok", 200
if __name__ == "__main__":
app.run(port=3000) Trigger a replay from the FilingIQ dashboard or via the API to exercise your receiver. The dashboard's deliveries log shows the exact bytes that were signed and the HTTP response your endpoint returned.
curl -X POST https://api.filingiq.io/v1/webhook_endpoints/<id>/deliveries/<delivery_id>/replay \
-H "Authorization: Bearer fiq_live_<key>" Rotating an endpoint's secret keeps the previous secret valid for 24 hours. Verify against both during the overlap window so deliveries in flight don't fail. The verifier above takes one secret at a time, so check the new secret first and fall back to the previous one.
function verifyWithOverlap(raw, header, current, previous) {
if (verifyWebhookSignature(raw, current, header)) return true;
if (previous && verifyWebhookSignature(raw, previous, header)) return true;
return false;
} express.raw for this reason.
toleranceSec only as a last resort.