Skip to content

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.)

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.

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
}
  • assessment is one of no_material_concerns | manual_review_recommended | material_inconsistency | assessment_incomplete — advisory; the decision is yours.
  • summary_reasons is a short, PII-free list of reason strings.
  • evidence_available tells you a full result exists — fetch it with GET /v1/verifications/{id} over your authenticated API connection. A webhook never embeds the full result.

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.

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.

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));
}

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.

  • Verify the signature before trusting the payload, and reject stale timestamps; every attempt is signed afresh.
  • Persist the event, then return 2xx quickly. Make handlers idempotent — drop a delivery whose X-Thibit-Delivery-Id you 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.