Developers API v1

Webhooks

Callbacks are the primary delivery mechanism. Everything an assessment produces reaches you this way, signed, with the GET endpoints as the recovery path.

Delivery model#

Every create returns 202 and the outcome arrives here, on a signed callback. One response shape, one delivery path, and nothing to branch on.

Event catalogue#

EventFires when, and what to do
assessment.screening.completed The first stage finished, in either mode. In screening mode, assessment.completed follows with the result. In full mode this is progress only: result is null until the assessment is complete
assessment.bank_data.required The assessment reached awaiting_bank_data. Start the borrower's account-information journey and submit the result before expires_at
assessment.bank_data.received Your submission was accepted and evaluation resumed. No action needed
assessment.completed Terminal. A result exists. This fires for a rejection too, because a rejection is a successful assessment whose answer is no
assessment.failed Terminal. No assessment was produced. Not a credit outcome. Treat as an operational event, not a decision

Payload shape#

Every event has the same envelope. data.object is the full assessment, the same one GET /v1/assessments/{id} returns, so a consumer that has verified the signature does not need a second call to act.

Your metadata is echoed on every event, so a callback can be correlated with your own case record without keeping a lookup table of Credit Atlas ids.

Signature verification#

Every callback carries:

CreditAtlas-Signature: t=<unix_timestamp>,v1=<hex>

The signature is HMAC-SHA256 over <timestamp> + "." + <raw body>, keyed with your signing secret. Three rules, and each one is there because skipping it breaks the guarantee:

RuleWhat goes wrong without it
Compute over the raw body Parsing and re-serialising JSON changes whitespace and key order. The bytes you sign stop being the bytes we signed, and every callback fails verification
Compare in constant time A short-circuiting comparison leaks how much of the signature was correct, which is enough to forge one given time
Reject a stale timestamp (300 s) Without it a valid callback captured once can be replayed at you indefinitely. The signature alone proves authenticity, not freshness

Retry schedule#

Seven attempts, at 0, 60, 300, 1800, 7200, 28800 and 86400 seconds after the event. That is immediately, then spreading out to a final try 24 hours later. Any 2xx within ten seconds stops them. A non-2xx, a timeout or a connection failure schedules the next attempt.

Acknowledge first and process afterwards. If your handler does the work before replying, a slow database turns into a duplicate delivery, and then into a second duplicate.

Deduplication#

Be idempotent on event.id

An acknowledgement that times out is retried even though the first delivery arrived and was processed. This is not an edge case. It is the normal behaviour of every at-least-once delivery system, including this one. Store event.id and ignore repeats.

When a callback is missed#

If your endpoint was down for all seven attempts, the event is gone. Recovery is GET /v1/assessments filtered by created_after and status.

Reconcile from the list, not from memory.

Your endpoint#

Common mistakes#

MistakeConsequence
Verifying against parsed-and-reserialised JSONEvery signature fails. The most common integration bug in any signed-webhook system
Processing before acknowledgingDuplicate deliveries under load, exactly when you can least afford them
Not storing event.idThe same assessment is actioned twice
Treating assessment.failed as a rejectionAn applicant is declined because a registry timed out
Treating assessment.completed as an approvalIt fires for every terminal outcome. Read policy_outcome.value
Waiting for expiredIt is a terminal state, not a signal to wait for. Stop waiting at expires_at
Skipping the timestamp checkCaptured callbacks can be replayed at you later