Webhooks
When a verification reaches a terminal status, thibit POSTs the verdict to your
configured webhook_url. Set it with POST /v1/webhook (or in the
portal).
Payload
{ "event": "verification.completed", "id": "…", "type": "kyc", "status": "completed", "decision": "aprovada", "confidence": 0.92, "result": { "verification_result": { "checks": {} } }}event is verification.<status> (e.g. verification.completed,
verification.failed).
Headers
| Header | Value |
|---|---|
X-Thibit-Event | verification.<status> |
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 webhook_secret (whsec_…) you received at signup 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));}Receiver behavior
- Verify the signature before trusting the payload, and reject stale timestamps.
- Persist the event, then return
2xx. Make handlers idempotent. - Treat the webhook as the trigger; if your business rules need fresh state,
re-fetch
GET /v1/verifications/{id}before acting. - Today delivery is a single attempt (retries are on the roadmap), so also reconcile by polling if a webhook is critical to your flow.