Webhooks
When a verification finishes processing, thibit POSTs the result to your
configured webhook URL. Configure it per project in the dashboard
(Integrate → Webhook) or with PUT /v1/projects/{project_id}/webhook. The
signing secret (whsec_…) is minted when the webhook is first set and
returned exactly once in that response — store it then. (regenerate=true mints
a fresh one; clearing the URL keeps the secret so re-enabling reuses it.)
Events
Section titled “Events”| Event | Fired when |
|---|---|
verification.completed |
The engine produced an assessment. |
verification.failed |
The verification ended without an assessment because processing failed or the document is not supported; assessment is assessment_incomplete. |
verification.reviewed |
A teammate recorded a customer disposition on a Case. |
A verification whose hosted link expires before the applicant submits also ends
failed with assessment_incomplete, but no webhook is sent for it. Poll
GET /v1/verifications/{id} to notice abandoned verifications.
Payload
Section titled “Payload”Every delivery carries a PII-free envelope — enough to act on, nothing to leak:
{ "event": "verification.completed", "id": "…", "type": "kyc", "status": "completed", "assessment": "no_material_concerns", "advisory": true, "confidence": 0.92, "summary_reasons": [], "evidence_available": true}assessmentis one ofno_material_concerns|manual_review_recommended|material_inconsistency|assessment_incomplete— advisory; the decision is yours.summary_reasonsis a short, PII-free list of reason strings.evidence_availabletells you a full result exists — fetch it withGET /v1/verifications/{id}over your authenticated API connection. A webhook never embeds the fullresult.
verification.reviewed additionally carries the team’s outcome, distinct from
the assessment: "customer_disposition" and "review": { "state", "at" }, where
the state is no_customer_concerns_recorded | customer_review_continues |
customer_concern_recorded.
Headers
Section titled “Headers”| Header | Value |
|---|---|
X-Thibit-Event |
The event name (e.g. verification.completed) |
X-Thibit-Delivery-Id |
The id of this delivery; every retry of it repeats the same id |
X-Thibit-Timestamp |
Unix seconds when this attempt was signed |
X-Thibit-Signature |
Hex HMAC-SHA256 (see below) |
A test delivery sent from the dashboard (event webhook.test) is signed the
same way, but has no delivery id and is not retried.
Verify the signature
Section titled “Verify the signature”The signature is HMAC_SHA256(secret, "{timestamp}.{raw_body}"), hex-encoded,
where secret is the whsec_… value you received when the webhook was set and
raw_body is the exact bytes of the request body. Compute it over the raw
body — do not re-serialize the parsed JSON.
import hmac, hashlib
def verify(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool: mac = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256) return hmac.compare_digest(mac.hexdigest(), signature)import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, timestamp, rawBody, signature) { const mac = createHmac("sha256", secret) .update(`${timestamp}.`).update(rawBody).digest("hex"); return timingSafeEqual(Buffer.from(mac), Buffer.from(signature));}Delivery and retries
Section titled “Delivery and retries”Each event is written to a durable queue in the same transaction as the change
it reports, and delivered from that queue. An attempt succeeds when your
endpoint answers 2xx; any other status, a redirect (redirects are not
followed), or no answer within the 10-second timeout fails it. A failed delivery
is retried, for up to 6 attempts over about 23 minutes (the retries wait
roughly 5 seconds, 30 seconds, 2 minutes, 5 minutes and 15 minutes); after the
sixth failure thibit stops trying. A queued event is dropped if its verification
is erased, or no webhook URL is configured any more, by the time it is sent.
A delivery may arrive more than once, with the same X-Thibit-Delivery-Id,
and it can also fail for good, so treat the webhook as a trigger and keep a
polling fallback for anything critical.
Receiver behavior
Section titled “Receiver behavior”- Verify the signature before trusting the payload, and reject stale timestamps; every attempt is signed afresh.
- Persist the event, then return
2xxquickly. Make handlers idempotent — drop a delivery whoseX-Thibit-Delivery-Idyou have already processed. - Treat the webhook as the trigger; for the full evidence (and fresh state),
fetch
GET /v1/verifications/{id}before acting. - Reconcile by polling: an expired link sends no webhook, and a delivery that failed all 6 attempts is not sent again.