Webhooks
CRELYTIC iQ Intel delivers Credit Drift Alerts to your endpoint as signed JSON POSTs. Every webhook carries an X-Intel-Signature header so you can verify the payload came from us — reject any request that doesn't verify.
Event format
POST body is JSON. Current event type: drift_alert.
{
"event": "drift_alert",
"alert": {
"id": 12345,
"company_name": "ExampleCorp Inc.",
"normalized_name": "examplecorp",
"score_before": 78,
"score_after": 63,
"delta": -15,
"threshold_pts": 10,
"window_days": 90,
"window_start": "2026-01-20",
"window_end": "2026-04-20",
"detected_at": "2026-04-20T14:22:00Z"
}
}Signature header
Every POST includes an X-Intel-Signature header in Stripe-compatible format:
X-Intel-Signature: t=1713714720,v1=a8b1c…t=— Unix timestamp (seconds since epoch) when we signedv1=— HMAC-SHA256 hex digest over{t}.{raw_body}
Verification — Node.js
import crypto from 'node:crypto'
function verifyIntelWebhook(
rawBody, // the exact bytes we POSTed — NOT parsed JSON
headerSig, // req.headers['x-intel-signature']
secret // shared secret from your Drift dashboard
) {
const parts = Object.fromEntries(
headerSig.split(',').map(p => p.split('='))
)
const t = parts.t
const v1 = parts.v1
if (!t || !v1) return false
// Reject if timestamp is older than 5 minutes (replay protection)
const ageSec = Math.floor(Date.now() / 1000) - Number(t)
if (Math.abs(ageSec) > 300) return false
const signed = `${t}.${rawBody}`
const expected = crypto
.createHmac('sha256', secret)
.update(signed)
.digest('hex')
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(v1, 'hex'),
Buffer.from(expected, 'hex')
)
}Verification — Python
import hmac
import hashlib
import time
def verify_intel_webhook(raw_body: bytes, header_sig: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header_sig.split(","))
t = parts.get("t")
v1 = parts.get("v1")
if not t or not v1:
return False
# Replay protection — reject if more than 5 minutes old
if abs(int(time.time()) - int(t)) > 300:
return False
signed = f"{t}.{raw_body.decode('utf-8')}".encode()
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(v1, expected)Important verification rules
- Sign the raw body bytes, not the re-serialized JSON. Your framework may re-order keys when it parses and re-stringifies — that breaks the signature.
- Reject requests older than 5 minutes. This prevents replay attacks if the original TLS session was intercepted.
- Use constant-time comparison(
timingSafeEqual/hmac.compare_digest). A naive===leaks signature bytes through timing. - Keep the secret out of logs and error pages. It's a shared secret — treat it like your DB password.
Retries + delivery guarantees
If your endpoint returns anything other than 2xx, we retry up to 5 times with exponential backoff. After 5 failures we stop retrying and mark the alert as undeliverable — the row stays in your Alert history for manual review.
Idempotency: alerts carry a uniquealert.id. Treat it as the dedup key. If a retry arrives after your endpoint already processed it, return 200 without side effects.
Your webhook secret
The signing secret is per-environment, not per-watchlist-entry. View and rotate it from your Drift Alerts dashboard (coming soon — contact support in the interim).