Webhooks
Webhook endpoints are configured per-account under Settings → Webhooks. When you create an endpoint you provide:
- URL — must be HTTPS, and must not point to a private/internal/loopback
host (
localhost,.local/.internalsuffixes, and private/loopback/ link-local IPs are all rejected).
Credlab generates a unique signing secret for each endpoint when it’s created; it’s used to sign every delivery (see Verifying signatures). The secret is available any time on the endpoint’s page (Settings → Webhooks → your endpoint).
How sends happen
Webhooks are staff-initiated, not automatic. A credit application’s dashboard
shows a Send to menu listing the account’s active webhook endpoints;
choosing one queues one delivery of the credit_application.decided event to
that endpoint. Staff typically send after recording a decision, but a send can
happen at any point — if no decision has been recorded yet, the payload’s
approval_status and approval_decided_at are null (or a non-decision
status such as pending).
Staff resending the same application after a correction is normal — a
correction to a decision, or simply re-pushing a decision, produces another
delivery for the same credit_application.id. Your receiver should process
each delivery idempotently: upsert by credit_application.id (the newest
delivery for a given id wins), and use the X-Credlab-Delivery header to
dedupe delivery attempts that are retries of the same send rather than
independent sends.
Event: credit_application.decided
This event carries a thin notification, not the full application — it tells
you what changed and where to fetch it, not the data itself. The request
is an HTTP POST to your configured URL.
Example payload body:
{
"event": "credit_application.decided",
"credit_application": {
"id": "ca_sample000000",
"approval_status": "approved",
"approval_decided_at": "2026-01-02T00:00:00Z"
},
"account_id": "acct_sample000000"
}
On receipt
- Verify the
X-Credlab-Signatureheader (see Verifying signatures). - Fetch the full record:
GET /api/v1/credit_applications/<credit_application.id>?account_id=<account_id>, using the ids from the payload body. - Upsert the fetched record into your system, keyed on
credit_application.id.
Request headers
| Header | Description |
|---|---|
Content-Type |
Always application/json. |
X-Credlab-Event |
The event type, e.g. credit_application.decided (or test — see the Testing section). |
X-Credlab-Delivery |
A unique id for this delivery attempt’s underlying delivery record (e.g. whd_1a2b3c4d5e6f). This id stays the same across retries of the same delivery — use it as an idempotency key so a retried delivery isn’t double-processed. |
X-Credlab-Signature |
The HMAC signature of this request. See Verifying signatures. |
Verifying signatures
Every webhook request includes an X-Credlab-Signature header in the form:
t=<unix timestamp>,v1=<hex-encoded HMAC-SHA256 digest>
The digest is computed over the string "<t>.<raw request body>", using
HMAC-SHA256 keyed with the endpoint’s signing secret (available any time on
the endpoint’s page, under Settings → Webhooks → your endpoint).
To verify a delivery:
received = header "X-Credlab-Signature" # "t=1730000000,v1=abc123..."
t = value after "t="
v1 = value after "v1="
expected = hex( HMAC_SHA256( key: signing_secret, data: t + "." + raw_request_body ) )
valid = constant_time_equal(expected, v1) AND (now - t) < 300 seconds
Important details:
- Use the raw, unparsed request body (exact bytes as received) when computing the digest — re-serializing parsed JSON can change whitespace and break the signature.
- Use a constant-time comparison to avoid timing attacks.
- Reject requests where the timestamp is too old (a 5-minute/300-second tolerance is recommended) to guard against replay of captured requests.
Delivery behavior
- Your endpoint must respond with a
2xxstatus within 10 seconds. Slower responses are treated as a timeout. - If your endpoint responds with a non-2xx status, times out, or the connection fails, Credlab retries the delivery with increasing backoff, up to 8 attempts total, spread over roughly 1.5 hours.
- If all attempts are exhausted without a successful
2xxresponse, the delivery is marked failed. Failed and past deliveries are visible in the delivery log on the webhook endpoint’s page under Settings → Webhooks, showing each delivery’s event, state, most recent response code, attempt count, and timestamp. - Because retries can occur, your receiver should be idempotent: use the
X-Credlab-Deliveryheader value to detect and ignore a delivery you’ve already processed. - Webhooks are a best-effort, at-least-once push mechanism, not a guaranteed
log. If your receiver has downtime longer than the retry window, or you
suspect a gap, use the read API’s
decided_sincefilter to poll for and backfill any decisions you may have missed.