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

EventSent when
fraud_alert_createdA new fraud alert was raised for one of your products.
fraud_alert_escalatedAn existing fraud alert was escalated.
recall_initiatedA batch recall was started.
code_job_completedA code generation job finished and its deliverable is ready.
report_receivedA consumer reported a concern about one of your products.
usage_thresholdYour company reached a usage threshold on its plan.
billing_eventSomething changed with your subscription or invoices.
platform_maintenanceCredChek 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 https and resolve to a public address. Private and internal network addresses are refused, and redirects are not followed.
  • Respond with any 2xx status within 10 seconds. Do slow work after you respond, for example in a queue.
  • Use POST /v1/webhooks/{id}/test to queue a test delivery (202), and POST /v1/webhooks/{id}/rotate-secret to 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 (two v1= values in the header), so you can switch over at any point in that window. Accept a match on any v1.

Delivery

Each delivery is a JSON POST with these headers:

http
Content-Type: application/json
X-CredCheck-Event: fraud_alert_created
X-CredCheck-Signature: t=1789651200,v1=5f2b…
Body
{
  "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:

  1. Read the raw request body as bytes, before any JSON parsing.
  2. Build the signed message: the value of t, a full stop, then the raw body ("<t>.<body>").
  3. Compute HMAC-SHA256 of that message with your endpoint secret and hex-encode it.
  4. Compare it with v1 using a constant-time comparison.
  5. Reject the delivery if t is more than 5 minutes (300 seconds) from your current time. This stops old deliveries being replayed.
Node.js (Express)
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);
});
Python (Flask)
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 "", 200

With 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}/replay once 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.

Next: TypeScript SDK

Webhooks · CredChek