Developers
Webhooks
Webhooks tell your systems about events as they happen, so you don’t need to poll. Every delivery is signed so you can prove it came from CredChek.
Events
| Event | Sent when |
|---|---|
fraud_alert_created | A new fraud alert was raised for one of your products. |
fraud_alert_escalated | An existing fraud alert was escalated. |
recall_initiated | A batch recall was started. |
code_job_completed | A code generation job finished and its deliverable is ready. |
report_received | A consumer reported a concern about one of your products. |
usage_threshold | Your company reached a usage threshold on its plan. |
billing_event | Something changed with your subscription or invoices. |
platform_maintenance | CredChek announced platform maintenance. |
Register an endpoint
Create endpoints with POST /v1/webhooks (scope webhooks:manage) or in the company portal. Choose at least one event. The response includes the endpoint’s signing secret, which starts with whsec_. It is encrypted at rest and this is the only time you see it: listing or updating an endpoint returns just secretHint, its last four characters. Store it like a password.
- URLs must use
httpsand resolve to a public address. Private and internal network addresses are refused, and redirects are not followed. - Respond with any
2xxstatus within 10 seconds. Do slow work after you respond, for example in a queue. - Use
POST /v1/webhooks/{id}/testto queue a test delivery (202), andPOST /v1/webhooks/{id}/rotate-secretto replace a secret. Rotation returns the new secret once. For 24 hours afterwards each delivery is signed with both the new and the old secret (twov1=values in the header), so you can switch over at any point in that window. Accept a match on anyv1.
Delivery
Each delivery is a JSON POST with these headers:
Content-Type: application/json
X-CredCheck-Event: fraud_alert_created
X-CredCheck-Signature: t=1789651200,v1=5f2b…{
"id": "…",
"type": "fraud_alert_created",
"severity": "critical",
"tenantId": "…",
"title": "…",
"data": { … },
"createdAt": "2026-09-17T10:00:00.000Z"
}severity is info, warning or critical. Use id to ignore duplicates: design your handler so receiving the same event twice is harmless.
Verify the signature
X-CredCheck-Signature has the form t=<unix seconds>,v1=<hex>. To verify a delivery:
- Read the raw request body as bytes, before any JSON parsing.
- Build the signed message: the value of
t, a full stop, then the raw body ("<t>.<body>"). - Compute HMAC-SHA256 of that message with your endpoint secret and hex-encode it.
- Compare it with
v1using a constant-time comparison. - Reject the delivery if
tis more than 5 minutes (300 seconds) from your current time. This stops old deliveries being replayed.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const secret = process.env.CREDCHECK_WEBHOOK_SECRET;
function verifySignature(rawBody, header, secret, toleranceSec = 300) {
if (!header) return false;
const parts = Object.fromEntries(
header.split(',').map((p) => {
const i = p.indexOf('=');
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
}),
);
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const received = parts.v1 ?? '';
if (!/^[0-9a-f]{64}$/i.test(received)) return false;
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'));
}
app.post('/webhooks/credcheck', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifySignature(rawBody, req.get('X-CredCheck-Signature'), secret)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(rawBody);
res.sendStatus(200); // respond quickly, then process
queueForProcessing(event);
});import hashlib
import hmac
import json
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["CREDCHECK_WEBHOOK_SECRET"]
def verify_signature(raw_body: bytes, header: str | None, secret: str, tolerance_sec: int = 300) -> bool:
if not header:
return False
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts.get("t", ""))
except ValueError:
return False
if abs(int(time.time()) - t) > tolerance_sec:
return False
message = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
@app.post("/webhooks/credcheck")
def credcheck_webhook():
raw_body = request.get_data() # raw bytes, before JSON parsing
if not verify_signature(raw_body, request.headers.get("X-CredCheck-Signature"), SECRET):
abort(400)
event = json.loads(raw_body)
enqueue_for_processing(event) # respond quickly, process later
return "", 200With the SDK, call verifyWebhookSignature({ payload, header, secret }). It returns true or false and never throws.
Retries and replay
If your endpoint doesn’t answer with 2xx, CredChek retries the delivery up to 6 attempts in total, waiting about 5 minutes, 15 minutes, 30 minutes, 1 hour and 2 hours between them. After the last attempt the delivery is kept as failed (dead letter) so you can replay it.
- List deliveries with
GET /v1/webhooks/deliveries. - Send a delivery again with
POST /v1/webhooks/deliveries/{id}/replayonce your endpoint is fixed. A replay starts a fresh set of attempts and is signed again with a new timestamp.
Keep secrets out of logs
Don’t log signing secrets or full signature headers. Rotate the secret if you think it has been exposed.