Skip to main content

Webhooks

Trusty sends a webhook to your endpoint when a verification completes (approved or rejected).

How it works

  1. Your user completes the flow in the widget (document + selfie)
  2. Trusty processes the images with AI
  3. When done, Trusty sends an HTTP POST to the URL you configured
  4. The webhook is signed with HMAC-SHA256 so you can verify it's from Trusty

Event

kyc.verification.completed

Signature header

X-Trusty-Signature: sha256=<hmac>

The webhook body is signed with HMAC-SHA256 using your webhook_secret. Always verify the signature before processing the payload.

Payload: approved verification

{
"event": "kyc.verification.completed",
"verification_id": "601e510f-b0c4-4f6d-a8a2-132fb9b40cf3",
"status": "approved",
"failure_reason": null,
"data": {
"document": {
"document_type": "national_id",
"issuing_country": "ESP",
"full_name": "GUILLERMO DANIEL CANELON LOVERA",
"document_number": "60518799A",
"date_of_birth": "1994-10-25",
"expiration_date": "2029-10-14",
"issue_date": "2024-10-14",
"readable": true,
"is_blurry": false,
"is_screen_recapture": false,
"is_photocopy": false,
"glare_detected": true
},
"liveness": {
"face_visible": true,
"single_face": true,
"eyes_visible": true,
"is_screen_recapture": false,
"is_printed_photo": false
},
"face_match": {
"same_person": true,
"confidence": "high"
}
},
"metadata": { "userId": "123" },
"completed_at": "2026-07-24T03:17:05.799Z"
}

Payload: rejected verification

{
"event": "kyc.verification.completed",
"verification_id": "6ce8ef26-8a2b-48d9-ab97-638efe144137",
"status": "rejected",
"failure_reason": "document_unreadable,screen_recapture_detected",
"data": {
"document": {
"readable": false,
"is_blurry": true,
"is_screen_recapture": true,
"full_name": null,
"document_number": null
},
"liveness": { "face_visible": false },
"face_match": { "same_person": false, "confidence": "low" }
},
"metadata": null,
"completed_at": "2026-07-24T02:53:54.767Z"
}

Payload fields

Top level

FieldDescription
eventAlways kyc.verification.completed
verification_idVerification UUID
statusapproved or rejected
failure_reasonIf rejected, comma-separated reasons. If approved, null
dataAnalysis results (see below)
metadataThe JSON you sent in the widget, unmodified
completed_atISO 8601 timestamp of when it completed

data.document

FieldDescription
document_typeDocument type: passport, national_id, drivers_license, other
issuing_countryIssuing country (ISO 3-letter code). null if unreadable
full_nameFull name extracted from the document. null if unreadable
document_numberDocument number. null if unreadable
date_of_birthDate of birth (YYYY-MM-DD). null if unreadable
expiration_dateExpiration date (YYYY-MM-DD). null if unreadable
issue_dateIssue date (YYYY-MM-DD). null if unreadable
readabletrue if the AI could read the document correctly
is_blurrytrue if the document was blurry
is_screen_recapturetrue if a screen photo was detected
is_photocopytrue if a photocopy was detected
glare_detectedtrue if glare was detected on the document

data.liveness

FieldDescription
face_visibletrue if a face was detected in the selfie video
single_facetrue if only one person was in the video
eyes_visibletrue if eyes were visible
is_screen_recapturetrue if the selfie is from a screen
is_printed_phototrue if the selfie is a printed photo

data.face_match

FieldDescription
same_persontrue if the selfie face matches the document photo
confidenceConfidence level: high, medium, low

Rejection reasons (failure_reason)

There may be one or several separated by comma:

ReasonDescription
document_unreadableThe AI could not read the document
document_blurryThe document was blurry
screen_recapture_detectedScreen photo detected on document or selfie
printed_photo_detectedPrinted photo detected on selfie
face_not_visibleNo face detected in the selfie
multiple_faces_detectedMore than one person in the selfie
face_mismatchSelfie face does not match the document
birth_date_unreadableDate of birth could not be read from the document

Signature verification

Node.js

import { createHmac, timingSafeEqual } from 'node:crypto'

const expected = createHmac('sha256', process.env.TRUSTY_WEBHOOK_SECRET)
.update(rawBody)
.digest('hex')

const valid = timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(`sha256=${expected}`)
)

Python

import hmac, hashlib

expected = hmac.new(
webhook_secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()

valid = hmac.compare_digest(signature_header, f"sha256={expected}")

PHP

$expected = hash_hmac('sha256', $rawBody, $webhookSecret);
$valid = hash_equals($signatureHeader, "sha256={$expected}");

Retries

  • 3 attempts with linear backoff (2s, 4s, 6s)
  • 4xx (except 429): no retry
  • 5xx, timeout, network error: retried
  • Per-attempt timeout: 10 seconds