Webhooks
Trusty sends a webhook to your endpoint when a verification completes (approved or rejected).
How it works
- Your user completes the flow in the widget (document + selfie)
- Trusty processes the images with AI
- When done, Trusty sends an HTTP
POSTto the URL you configured - 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
| Field | Description |
|---|---|
event | Always kyc.verification.completed |
verification_id | Verification UUID |
status | approved or rejected |
failure_reason | If rejected, comma-separated reasons. If approved, null |
data | Analysis results (see below) |
metadata | The JSON you sent in the widget, unmodified |
completed_at | ISO 8601 timestamp of when it completed |
data.document
| Field | Description |
|---|---|
document_type | Document type: passport, national_id, drivers_license, other |
issuing_country | Issuing country (ISO 3-letter code). null if unreadable |
full_name | Full name extracted from the document. null if unreadable |
document_number | Document number. null if unreadable |
date_of_birth | Date of birth (YYYY-MM-DD). null if unreadable |
expiration_date | Expiration date (YYYY-MM-DD). null if unreadable |
issue_date | Issue date (YYYY-MM-DD). null if unreadable |
readable | true if the AI could read the document correctly |
is_blurry | true if the document was blurry |
is_screen_recapture | true if a screen photo was detected |
is_photocopy | true if a photocopy was detected |
glare_detected | true if glare was detected on the document |
data.liveness
| Field | Description |
|---|---|
face_visible | true if a face was detected in the selfie video |
single_face | true if only one person was in the video |
eyes_visible | true if eyes were visible |
is_screen_recapture | true if the selfie is from a screen |
is_printed_photo | true if the selfie is a printed photo |
data.face_match
| Field | Description |
|---|---|
same_person | true if the selfie face matches the document photo |
confidence | Confidence level: high, medium, low |
Rejection reasons (failure_reason)
There may be one or several separated by comma:
| Reason | Description |
|---|---|
document_unreadable | The AI could not read the document |
document_blurry | The document was blurry |
screen_recapture_detected | Screen photo detected on document or selfie |
printed_photo_detected | Printed photo detected on selfie |
face_not_visible | No face detected in the selfie |
multiple_faces_detected | More than one person in the selfie |
face_mismatch | Selfie face does not match the document |
birth_date_unreadable | Date 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