Skip to main content

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

EventVerdictWhen fired
verification.approvedapprovedConfidence score above approval threshold, no critical flags
verification.rejectedrejectedConfidence below rejection threshold, or critical flag triggered
verification.review_requiredreviewConfidence 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:

FieldTypeAlwaysDescription
eventstringYesEvent type — switch on this in your handler
verificationIdstringYesUnique ID from /init (format: vf_*)
tenantIdstringYesYour tenant ID (useful for multi-tenant routing)
userRefstring | nullYesThe userRef you passed to /init, or null if you didn't
verdictstringYesapproved, review, or rejected
confidencenumberYesOverall weighted confidence score (0-100)
scoresobjectYesBreakdown of individual signals
flagsarrayYesQuality / risk flags raised. Empty [] when clean
metadataobjectYesThe metadata you passed to /submit, or {}
submittedAtstringYesISO 8601 timestamp when /submit was called
completedAtstringYesISO 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:

FieldRangeDescription
ocrConfidence0-100Self-reported VLM confidence in document text extraction
faceMatch0-100Biometric similarity between selfie and document photo
liveness0-100Anti-spoofing — confirms it's a live person, not a photo
docQuality0-100Image 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:

LevelMeaning
infoFYI for reviewers, doesn't affect verdict on its own
warnNotable issue — contributes to lowered confidence
criticalHard failure — pushes verdict to rejected regardless of other signals

Common flags you may see in the flags array:

FlagLevelMeaning
heavy_glarewarnDocument has reflective glare obscuring text
low_face_matchwarn / criticalSelfie and document photo don't match
low_livenesswarnAnti-spoofing score below threshold
low_doc_qualitywarnDocument image too blurry or low-resolution
expired_documentcriticalDocument expiry date has passed
mrz_mismatchcriticalMRZ checksum failed (potentially tampered passport)
name_mismatchwarnSubmitted name doesn't fuzzy-match OCR'd name
age_under_minimumcriticalCalculated 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 completing
  • verification.refunded — credit refunded due to internal error

What's next