Webhooks
Este conteúdo não está disponível em sua língua ainda.
When a verification reaches a terminal status, 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
| Event | Fired when |
|---|---|
verification.completed | The engine produced an assessment. |
verification.failed | The engine errored terminally. |
verification.reviewed | A teammate recorded a customer disposition on a Case. |
verification.kyb_completed | A KYB roll-up resolved — KYB is not in this release; production rejects type: "kyb", so this event does not fire today. |
Payload
The default payload is 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.- The full
resultobject is embedded only if your project has explicitly opted in to PII in webhooks; by default it is never included.
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
| Header | Value |
|---|---|
X-Thibit-Event | The event name (e.g. verification.completed) |
X-Thibit-Timestamp | Unix seconds when the delivery was signed |
X-Thibit-Signature | Hex HMAC-SHA256 (see below) |
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
Delivery is best-effort: up to 3 attempts with short backoff (roughly 7
seconds end-to-end), then thibit logs the failure and moves on. A 2xx from your
endpoint stops the retries. Because delivery is not guaranteed, treat the webhook
as a trigger and keep a polling fallback for anything critical.
Receiver behavior
- Verify the signature before trusting the payload, and reject stale timestamps.
- Persist the event, then return
2xxquickly. Make handlers idempotent — retries can deliver the same event more than once. - Treat the webhook as the trigger; for the full evidence (and fresh state),
fetch
GET /v1/verifications/{id}before acting. - Reconcile by polling if a webhook is critical to your flow.