Event types
Veridia emits three event types today, all of them about verification outcomes. Every event has a consistent base shape with event-specific fields layered on top.
Quick reference
| Event | Verdict | When fired |
|---|---|---|
verification.approved | approved | Confidence score above approval threshold, no critical flags |
verification.rejected | rejected | Confidence below rejection threshold, or critical flag triggered |
verification.review_required | review | Confidence in the middle band — manual review needed |
The default thresholds are configurable per tenant. Out of the box: rejected <60, review 60-80, approved >=80.
Common fields
Every event includes these fields:
| Field | Type | Always | Description |
|---|---|---|---|
event | string | Yes | Event type — switch on this in your handler |
verificationId | string | Yes | Unique ID from /init (format: vf_*) |
tenantId | string | Yes | Your tenant ID (useful for multi-tenant routing) |
userRef | string | null | Yes | The userRef you passed to /init, or null if you didn't |
verdict | string | Yes | approved, review, or rejected |
confidence | number | Yes | Overall weighted confidence score (0-100) |
scores | object | Yes | Breakdown of individual signals |
flags | array | Yes | Quality / risk flags raised. Empty [] when clean |
metadata | object | Yes | The metadata you passed to /submit, or {} |
submittedAt | string | Yes | ISO 8601 timestamp when /submit was called |
completedAt | string | Yes | ISO 8601 timestamp when verdict was determined |
The body is always compact JSON with sorted keys — required for reproducible signature verification.
verification.approved
Sent when a verification passes all signal thresholds and has no critical flags. This is the green-light event for your onboarding flow.
{
"event": "verification.approved",
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"tenantId": "tn_default_demo",
"userRef": "customer-12345",
"verdict": "approved",
"confidence": 87.4,
"scores": {
"ocrConfidence": 78.0,
"faceMatch": 96.2,
"liveness": 91.5,
"docQuality": 85.0
},
"flags": [],
"metadata": {
"campaign": "spring_2026",
"platform": "web"
},
"submittedAt": "2026-05-01T18:39:05Z",
"completedAt": "2026-05-01T18:39:08Z"
}
Typical handler:
case 'verification.approved':
await db.users.update(payload.userRef, {
kycStatus: 'verified',
kycCompletedAt: payload.completedAt,
kycVerificationId: payload.verificationId,
});
await sendWelcomeEmail(payload.userRef);
break;
verification.rejected
Sent when a verification fails — either confidence below the rejection threshold, or a critical flag (expired document, MRZ mismatch, age below minimum, etc.).
{
"event": "verification.rejected",
"verificationId": "vf_BX18DEXSGFRX5U16YH3",
"tenantId": "tn_default_demo",
"userRef": "customer-67890",
"verdict": "rejected",
"confidence": 42.1,
"scores": {
"ocrConfidence": 65.0,
"faceMatch": 31.4,
"liveness": 88.0,
"docQuality": 50.5
},
"flags": [
{ "level": "critical", "text": "low_face_match" },
{ "level": "warn", "text": "low_doc_quality" }
],
"metadata": {},
"submittedAt": "2026-05-01T19:02:12Z",
"completedAt": "2026-05-01T19:02:15Z"
}
Typical handler:
case 'verification.rejected':
await db.users.update(payload.userRef, {
kycStatus: 'rejected',
kycRejectionReasons: payload.flags,
});
await sendRejectionEmail(payload.userRef, payload.flags);
// Don't tell the user the exact flags — security risk
break;
:::warning Don't surface flags to end users Telling a fraudster which signal caught them helps them craft a better attempt. Show end users a generic "verification failed, please contact support" — keep the detailed flags for your internal review queue. :::
verification.review_required
Sent when confidence falls in the middle band. The user might be legitimate but the system isn't confident enough to auto-approve. Your reviewers should look at it.
{
"event": "verification.review_required",
"verificationId": "vf_CY29EFYTGFSZ6V27ZH4",
"tenantId": "tn_default_demo",
"userRef": "customer-11111",
"verdict": "review",
"confidence": 67.3,
"scores": {
"ocrConfidence": 72.0,
"faceMatch": 79.5,
"liveness": 88.0,
"docQuality": 45.0
},
"flags": [
{ "level": "warn", "text": "heavy_glare" },
{ "level": "info", "text": "name_mismatch" }
],
"metadata": {},
"submittedAt": "2026-05-01T20:15:30Z",
"completedAt": "2026-05-01T20:15:33Z"
}
Typical handler:
case 'verification.review_required':
await db.users.update(payload.userRef, {
kycStatus: 'pending_review',
});
await reviewQueue.add({
verificationId: payload.verificationId,
userRef: payload.userRef,
flags: payload.flags,
priority: payload.flags.some(f => f.level === 'critical') ? 'high' : 'normal',
});
// Optionally pause sensitive actions until reviewer decides
break;
Scores breakdown
The scores object is the same shape across all events:
| Field | Range | Description |
|---|---|---|
ocrConfidence | 0-100 | Self-reported VLM confidence in document text extraction |
faceMatch | 0-100 | Biometric similarity between selfie and document photo |
liveness | 0-100 | Anti-spoofing — confirms it's a live person, not a photo |
docQuality | 0-100 | Image quality of the captured document (sharpness, lighting, framing) |
The overall confidence is a weighted combination — primarily faceMatch and liveness, with ocrConfidence and docQuality as supporting signals.
Flags reference
Flags fire when an individual signal falls below threshold or a hard check fails. Each has a level:
| Level | Meaning |
|---|---|
info | FYI for reviewers, doesn't affect verdict on its own |
warn | Notable issue — contributes to lowered confidence |
critical | Hard failure — pushes verdict to rejected regardless of other signals |
Common flags you may see in the flags array:
| Flag | Level | Meaning |
|---|---|---|
heavy_glare | warn | Document has reflective glare obscuring text |
low_face_match | warn / critical | Selfie and document photo don't match |
low_liveness | warn | Anti-spoofing score below threshold |
low_doc_quality | warn | Document image too blurry or low-resolution |
expired_document | critical | Document expiry date has passed |
mrz_mismatch | critical | MRZ checksum failed (potentially tampered passport) |
name_mismatch | warn | Submitted name doesn't fuzzy-match OCR'd name |
age_under_minimum | critical | Calculated age below tenant's minimum (default 18) |
Routing pattern
Most production handlers route on the event type:
async function handleVeridiaWebhook(payload) {
switch (payload.event) {
case 'verification.approved':
return handleApproved(payload);
case 'verification.rejected':
return handleRejected(payload);
case 'verification.review_required':
return handleReviewRequired(payload);
default:
// Future-proofing: log unknown events but don't fail
logger.warn('Unknown Veridia event type', {
event: payload.event,
verificationId: payload.verificationId,
});
}
}
We may add new event types in the future (e.g., verification.expired, verification.refunded). Handle unknown event types gracefully — log and ignore, don't error.
Future events (not yet emitted)
These are documented for forward compatibility. Don't write code that depends on them yet:
verification.expired— verification timed out without completingverification.refunded— credit refunded due to internal error
What's next
- Examples — full handler implementations
- Signature verification — algorithm reference
- Webhooks overview — back to the overview