Q4API Docs
Q4 APISign inGet API key
DocumentationAPI Reference
Overview
IntroductionQuickstartData overviewPlans and limits
Datasets
CompaniesMarket dataRealtime & streamingFundamentalsDividends & corporate actionsFilings & reportsBoard reportsPresentationsEarnings callsAnnouncementsEconomic data
REST API
AuthenticationPagination and filteringRate limitsErrors
WebSocket
ConnectingStreamsWebSocket errors
Webhooks
WebhooksWebhook eventsSignatures
MCP
Q4 MCP connector
More
ChangelogSupport
Webhooks

Signatures

Verify that a webhook came from Q4.

Every delivery has an X-Q4-Signature header:

X-Q4-Signature: t=1762228931,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 is an HMAC-SHA256 of {t}.{raw body}, keyed with your webhook's signing_secret.

  1. Split the header on , to get t and v1.
  2. Reject it if t is more than 5 minutes old.
  3. Compute the HMAC of t + "." + body using the raw request body, before any JSON parsing.
  4. Compare it to v1 in constant time.
import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(
        secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
← Previous
Webhook events
Next →
Q4 MCP connector