Verifying webhook signatures

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.

Header format

http
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.

Node.js

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.

receiver.ts
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);

Python

Standard-library Python plus Flask. No third-party crypto dependency.

receiver.py
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)

Curl test

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.

bash
curl -X POST https://api.filingiq.io/v1/webhook_endpoints/<id>/deliveries/<delivery_id>/replay \
  -H "Authorization: Bearer fiq_live_<key>"

Secret rotation

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.

typescript
function verifyWithOverlap(raw, header, current, previous) {
  if (verifyWebhookSignature(raw, current, header)) return true;
  if (previous && verifyWebhookSignature(raw, previous, header)) return true;
  return false;
}

Troubleshooting