Flow Examples
Battle-tested verification flow patterns for real-world use cases, including document scanning and two-factor authentication.
Overview
FaceSign's node-based flow system allows you to create sophisticated verification workflows tailored to your specific requirements. Here are battle-tested flows for common use cases.
Video-analysis nodes — liveness_detection, face_scan, face_compare, and recognition — need at least 5 seconds of accumulated camera feed before they can run. A conversation node with actual avatar speech is the canonical way to set up that buffer and must precede the first video-analysis node in the flow.
Other nodes that keep the camera live during their UI — two_factor_email and two_factor_sms — also accumulate camera feed, so when they sit between the start of the session and the first video-analysis node they can substitute for the greeting conversation.
Once the buffer is established, subsequent video-analysis nodes chain back-to-back without another conversation in between. See Conversation Rules → Greeting length for the full rule.
Basic Step-Up Verification
Simple Liveness Check
Confirm a real person is in front of the camera and rule out photos, videos, or deepfake spoofs. Liveness detection requires camera access and at least 5 seconds of accumulated video feed, so a short greeting CONVERSATION must precede the liveness node to give the camera time to warm up.
import { Client, FSNodeType } from '@facesignai/api'
const client = new Client({
auth: process.env.FACESIGN_API_KEY
})
const { session, clientSecret } = await client.session.create({
clientReferenceId: 'user-123',
flow: [
{
id: 'start',
type: FSNodeType.START,
outcome: 'greeting'
},
{
// Greeting fills the 5+ seconds of camera feed that the liveness
// model needs before it can run.
id: 'greeting',
type: FSNodeType.CONVERSATION,
prompt: "Say: Hi! I just need to take a quick look to confirm you're really you. It only takes a few seconds.",
outcomes: [{ id: 'next', targetNodeId: 'liveness', condition: '' }]
},
{
id: 'liveness',
type: FSNodeType.LIVENESS_DETECTION,
outcomes: {
livenessDetected: 'closing',
deepfakeDetected: 'closing',
noFace: 'closing'
}
},
{
// Final closing turn. doesNotRequireReply + single unconditional
// outcome to END is the canonical farewell pattern.
id: 'closing',
type: FSNodeType.CONVERSATION,
prompt: "Thank the user for completing the verification and tell them the session is now complete. Keep it to one sentence.",
doesNotRequireReply: true,
outcomes: [{ id: 'done', targetNodeId: 'end', condition: '' }]
},
{
id: 'end',
type: FSNodeType.END
}
]
})Document Verification with Selfie
Standard KYC flow: verify government ID and match it with a selfie.
const { session, clientSecret } = await client.session.create({
clientReferenceId: 'user-123',
flow: [
{
id: 'start',
type: FSNodeType.START,
outcome: 'greeting'
},
{
// Greeting establishes the 5+ seconds of camera feed required by
// the first video-analysis node (`face` / FACE_SCAN below).
id: 'greeting',
type: FSNodeType.CONVERSATION,
prompt: "Say: Hi! I'll quickly verify your identity. First I'll scan your ID, then take a quick look at your face. It only takes a few seconds.",
outcomes: [{ id: 'next', targetNodeId: 'doc', condition: '' }]
},
{
id: 'doc',
type: FSNodeType.DOCUMENT_SCAN,
scanningMode: 'single',
allowedDocumentTypes: ['id', 'passport'],
outcomes: {
scanSuccess: 'face',
userCancelled: 'closing',
scanTimeout: 'closing'
}
},
{
// Captures a high-quality selfie via the oval capture UI.
id: 'face',
type: FSNodeType.FACE_SCAN,
outcomes: {
passed: 'face_compare',
notPassed: 'closing',
cancelled: 'closing',
error: 'closing'
}
},
{
// Compares the selfie from `face` against the photo extracted
// from the scanned document.
id: 'face_compare',
type: FSNodeType.FACE_COMPARE,
sourceA: { source: 'faceScan' },
sourceB: { source: 'documentPhoto' },
similarityThreshold: 0.7,
outcomes: {
match: 'liveness',
noMatch: 'closing',
imageUnavailable: 'closing'
}
},
{
id: 'liveness',
type: FSNodeType.LIVENESS_DETECTION,
outcomes: {
livenessDetected: 'closing',
deepfakeDetected: 'closing',
noFace: 'closing'
}
},
{
id: 'closing',
type: FSNodeType.CONVERSATION,
prompt: "Thank the user for completing the verification and tell them the session is now complete. Keep it to one sentence.",
doesNotRequireReply: true,
outcomes: [{ id: 'done', targetNodeId: 'end', condition: '' }]
},
{
id: 'end',
type: FSNodeType.END
}
]
})Document Scanning Flow
When using document_scan followed by face_compare, prep the user with a conversation node before launching the scanner. The face_scan node triggers a dedicated selfie capture UI -- if you do not need an explicit selfie step, skip it. Biometrics are captured naturally during any session, and recognition on subsequent sessions will match against faces seen before without an explicit enrollment step.
{ id: 'doc_prep', type: 'conversation',
prompt: "Say: I'll need to scan your government issued ID. Please have it ready.",
outcomes: [{ id: 'next', targetNodeId: 'doc_scan', condition: '' }] },
{ id: 'doc_scan', type: 'document_scan',
scanningMode: 'automatic',
allowedDocumentTypes: ['id', 'passport', 'dl'],
outcomes: { scanSuccess: 'face_compare', userCancelled: 'closing', scanTimeout: 'closing' } },
{ id: 'face_compare', type: 'face_compare',
sourceA: { source: 'documentPhoto' },
sourceB: { source: 'sessionVideo' },
similarityThreshold: 0.7,
outcomes: { match: 'closing', noMatch: 'closing', imageUnavailable: 'closing' } },The conversation node before document_scan uses an empty string condition (condition: '') for unconditional advancement -- the avatar speaks the prep message and immediately transitions to the scanner.
Two-Factor Authentication
Both two_factor_email and two_factor_sms collect a contact and verify an OTP.
Prefer enter_email followed by two_factor_email for most integrations. Email OTPs deliver reliably, the form-to-OTP handoff works cleanly, and you avoid SMS delivery costs.
two_factor_sms currently has a known form-to-speech handoff timing issue where the avatar occasionally asks for the phone number verbally after the form submit. If SMS is required, audit the transition carefully before production use.
Multi-Factor Authentication Flow
Combine email and SMS verification with biometric checks.
const { session, clientSecret } = await client.session.create({
clientReferenceId: 'user-123',
providedData: {
email: 'user@example.com',
phone: '+1234567890'
},
flow:[
{
id: 'start',
type: FSNodeType.START,
outcome: 'email_otp'
},
{
id: 'email_otp',
type: FSNodeType.TWO_FACTOR_EMAIL,
outcomes: {
verified: 'sms_otp',
failed_unverified: 'closing',
delivery_failed: 'closing',
cancelled: 'closing',
error: 'closing'
}
},
{
id: 'sms_otp',
type: FSNodeType.TWO_FACTOR_SMS,
outcomes: {
verified: 'liveness',
failed_unverified: 'closing',
delivery_failed: 'closing',
cancelled: 'closing',
error: 'closing'
}
},
{
// No greeting CONVERSATION needed: TWO_FACTOR_EMAIL and
// TWO_FACTOR_SMS keep the camera live throughout OTP entry, so
// the 5+ second video buffer is already accumulated by the time
// liveness runs.
id: 'liveness',
type: FSNodeType.LIVENESS_DETECTION,
outcomes: {
livenessDetected: 'closing',
deepfakeDetected: 'closing',
noFace: 'closing'
}
},
{
id: 'closing',
type: FSNodeType.CONVERSATION,
prompt: "Thank the user for completing the verification and tell them the session is now complete. Keep it to one sentence.",
doesNotRequireReply: true,
outcomes: [{ id: 'done', targetNodeId: 'end', condition: '' }]
},
{
id: 'end',
type: FSNodeType.END
}
]
})Advanced Verification Flows
Conversational AI Verification
Use AI-powered conversation for dynamic verification questions.
const { session, clientSecret } = await client.session.create({
clientReferenceId: 'user-123',
avatarId: '65f9e3c9-d48b-4118-b73a-4ae2e3cbb8f0', // June HR — see facesign://catalog for the full list
flow: [
{
id: 'start',
type: FSNodeType.START,
outcome: 'verify'
},
{
// The opening sentences of this prompt establish the 5+ seconds
// of camera feed needed by the liveness node further down the flow.
id: 'verify',
type: FSNodeType.CONVERSATION,
prompt: "Warmly greet the user and explain you need to quickly verify their identity. Walk them through what's next — a couple of confirmation questions, then a document scan. Then ask them to confirm their full name and date of birth.",
outcomes: [
{
id: 'verified',
targetNodeId: 'doc',
condition: 'The user provided their name and date of birth'
},
{
id: 'notVerified',
targetNodeId: 'closing',
condition: 'The user refused or did not provide the information after two attempts'
}
]
},
{
id: 'doc',
type: FSNodeType.DOCUMENT_SCAN,
scanningMode: 'automatic',
allowedDocumentTypes: ['passport', 'dl', 'id'],
outcomes: {
scanSuccess: 'liveness',
userCancelled: 'closing',
scanTimeout: 'closing'
}
},
{
id: 'liveness',
type: FSNodeType.LIVENESS_DETECTION,
outcomes: {
livenessDetected: 'closing',
deepfakeDetected: 'closing',
noFace: 'closing'
}
},
{
id: 'closing',
type: FSNodeType.CONVERSATION,
prompt: "Thank the user for completing the verification and tell them the session is now complete. Keep it to one sentence.",
doesNotRequireReply: true,
outcomes: [{ id: 'done', targetNodeId: 'end', condition: '' }]
},
{
id: 'end',
type: FSNodeType.END
}
]
})Industry-Specific Flows
Financial Services KYC
Complete KYC flow for banking and financial services with enhanced fraud detection.
const { session, clientSecret } = await client.session.create({
clientReferenceId: 'your_internal_user_id',
metadata: {
applicationId: '789',
productType: 'checking_account',
riskLevel: 'standard'
},
flow: [
{
id: 'start',
type: FSNodeType.START,
outcome: 'greeting'
},
{
id: 'greeting',
type: FSNodeType.CONVERSATION,
prompt: 'Welcome to our KYC verification process. I will guide you through verifying your identity for your new account. First, please prepare your government-issued ID.',
outcomes: [
{
id: 'ready',
targetNodeId: 'doc',
condition: ''
}
]
},
{
id: 'doc',
type: FSNodeType.DOCUMENT_SCAN,
scanningMode: 'automatic',
allowedDocumentTypes: ['passport', 'driver-card', 'id'],
showTorchButton: true,
showCameraSwitch: true,
outcomes: {
scanSuccess: 'age_validation',
userCancelled: 'closing',
scanTimeout: 'closing'
}
},
{
id: 'age_validation',
type: FSNodeType.DATA_VALIDATION,
validation: {
field: 'age',
action: 'greaterThanOrEqual',
value: '18'
},
// DATA_VALIDATION outcomes route on the literal strings 'true' / 'false'
// returned by the validator — not on natural-language conditions.
outcomes: [
{
id: 'age_valid',
targetNodeId: 'face_scan',
condition: 'true'
},
{
id: 'age_invalid',
targetNodeId: 'closing',
condition: 'false'
}
]
},
{
// High-quality selfie capture via the oval UI.
id: 'face_scan',
type: FSNodeType.FACE_SCAN,
captureInstructions: 'Please position your face in the oval and hold steady',
outcomes: {
passed: 'face_compare',
notPassed: 'closing',
cancelled: 'closing',
error: 'closing'
}
},
{
// Compares the captured selfie against the photo extracted from
// the scanned document.
id: 'face_compare',
type: FSNodeType.FACE_COMPARE,
sourceA: { source: 'faceScan' },
sourceB: { source: 'documentPhoto' },
similarityThreshold: 0.7,
outcomes: {
match: 'liveness',
noMatch: 'closing',
imageUnavailable: 'closing'
}
},
{
id: 'liveness',
type: FSNodeType.LIVENESS_DETECTION,
outcomes: {
livenessDetected: 'recognition',
deepfakeDetected: 'closing',
noFace: 'closing'
}
},
{
id: 'recognition',
type: FSNodeType.RECOGNITION,
outcomes: {
newUser: 'two_factor',
recognized: 'closing',
noFace: 'closing'
}
},
{
id: 'two_factor',
type: FSNodeType.TWO_FACTOR_EMAIL,
outcomes: {
verified: 'closing',
delivery_failed: 'closing',
failed_unverified: 'closing',
cancelled: 'closing',
error: 'closing'
}
},
{
id: 'closing',
type: FSNodeType.CONVERSATION,
prompt: "Thank the user for completing the verification and tell them the session is now complete. Keep it to one sentence.",
doesNotRequireReply: true,
outcomes: [{ id: 'done', targetNodeId: 'end', condition: '' }]
},
{
id: 'end',
type: FSNodeType.END
}
],
videoAIAnalysisEnabled: true,
zone: 'us',
})Healthcare Patient Verification
HIPAA-compliant patient verification flow.
const { session, clientSecret } = await client.session.create({
clientReferenceId: 'your_internal_user_id',
metadata: {
patientId: 'patientId',
appointmentId: 'appointmentId',
provider: 'Dr. Smith',
verificationType: 'telehealth'
},
flow: [
{
id: 'start',
type: FSNodeType.START,
outcome: 'verify_identity'
},
{
id: 'verify_identity',
type: FSNodeType.CONVERSATION,
prompt: 'Greet the patient warmly and explain you need to verify their identity for telehealth appointment security. Ask them to state their full name and date of birth. Take a couple of sentences of context before the question.',
outcomes: [
{
id: 'info_provided',
targetNodeId: 'confirm_details',
condition: 'Patient stated both a full name and a date of birth in the same turn'
},
{
id: 'info_incomplete',
targetNodeId: 'retry_verification',
condition: 'Patient answered but did not provide both name and date of birth'
},
{
id: 'stall',
targetNodeId: 'closing',
condition: 'Conversation reached 4 exchanges with no useful answer'
}
]
},
{
// Announce-before-transactional pattern: the avatar speaks one line,
// then the flow moves unconditionally to the document scanner.
id: 'confirm_details',
type: FSNodeType.CONVERSATION,
prompt: "Tell the patient you'll now scan their ID. Ask them to have their driver's license, passport, or state ID ready. Keep it to one sentence.",
outcomes: [
{ id: 'next', targetNodeId: 'doc_scan', condition: '' }
]
},
{
id: 'retry_verification',
type: FSNodeType.CONVERSATION,
prompt: 'Politely remind the patient that verification is required to proceed. Ask once more for their full name and date of birth. Keep it brief.',
outcomes: [
{
id: 'retry_success',
targetNodeId: 'confirm_details',
condition: 'Patient stated both a full name and a date of birth'
},
{
id: 'retry_failed',
targetNodeId: 'closing',
condition: 'Patient answered but still did not provide both name and date of birth'
},
{
id: 'retry_stall',
targetNodeId: 'closing',
condition: 'Conversation reached 3 exchanges with no useful answer'
}
]
},
{
id: 'doc_scan',
type: FSNodeType.DOCUMENT_SCAN,
scanningMode: 'automatic',
allowedDocumentTypes: ['driver-card', 'passport', 'id'],
showTorchButton: true,
showCameraSwitch: true,
outcomes: {
scanSuccess: 'face_capture',
userCancelled: 'closing',
scanTimeout: 'closing'
}
},
{
// High-quality selfie capture via the oval UI.
id: 'face_capture',
type: FSNodeType.FACE_SCAN,
captureInstructions: 'Please position your face in the oval for verification',
outcomes: {
passed: 'face_compare',
notPassed: 'closing',
cancelled: 'closing',
error: 'closing'
}
},
{
// Compares the captured selfie against the photo extracted from
// the scanned document.
id: 'face_compare',
type: FSNodeType.FACE_COMPARE,
sourceA: { source: 'faceScan' },
sourceB: { source: 'documentPhoto' },
similarityThreshold: 0.75,
outcomes: {
match: 'closing',
noMatch: 'closing',
imageUnavailable: 'closing'
}
},
{
id: 'closing',
type: FSNodeType.CONVERSATION,
prompt: "Thank the patient for completing the verification and tell them the session is now complete. Keep it to one sentence.",
doesNotRequireReply: true,
outcomes: [{ id: 'done', targetNodeId: 'end', condition: '' }]
},
{
id: 'end',
type: FSNodeType.END
}
],
videoAIAnalysisEnabled: false,
customization: {
permissionsPage: {
backgroundType: 'COLOR',
backgroundColor: '#0066CC',
mainHeading: 'Welcome to Hospital Verification',
subheading: 'To start your telehealth appointment, please follow the verification steps.',
buttonText: 'Begin Verification'
},
controls: {
showUxControls: true
}
}
})Age Verification for Restricted Content
Quick age verification for alcohol, gambling, or adult content.
const { session, clientSecret } = await client.session.create({
clientReferenceId: 'internal_userId',
metadata: {
purpose: 'age_verification',
minAge: 21,
platform: 'web'
},
flow: [
{
id: 'start',
type: FSNodeType.START,
outcome: 'age_warning'
},
{
id: 'age_warning',
type: FSNodeType.CONVERSATION,
prompt: 'Welcome! This content is age-restricted. You must be 21 or older to proceed. I will need to verify your age using a government-issued ID.',
// Mid-flow informational turn — single unconditional outcome with condition: ''.
// doesNotRequireReply is reserved for the final node before END, so it stays unset here.
outcomes: [
{
id: 'proceed',
targetNodeId: 'doc',
condition: ''
}
]
},
{
id: 'doc',
type: FSNodeType.DOCUMENT_SCAN,
scanningMode: 'automatic',
allowedDocumentTypes: ['driver-card', 'passport', 'id'],
showTorchButton: true,
showCameraSwitch: false,
outcomes: {
scanSuccess: 'age_check',
userCancelled: 'closing',
scanTimeout: 'closing'
}
},
{
id: 'age_check',
type: FSNodeType.DATA_VALIDATION,
validation: {
field: 'age',
action: 'greaterThanOrEqual',
value: '21'
},
// DATA_VALIDATION outcomes route on the literal strings 'true' / 'false'
// returned by the validator — not on natural-language conditions.
outcomes: [
{
id: 'age_valid',
targetNodeId: 'liveness',
condition: 'true'
},
{
id: 'age_invalid',
targetNodeId: 'underage_message',
condition: 'false'
}
]
},
{
id: 'underage_message',
type: FSNodeType.CONVERSATION,
prompt: 'Sorry, you must be 21 or older to access this content. Your verification shows you do not meet the age requirement.',
doesNotRequireReply: true,
outcomes: [
{
id: 'end',
targetNodeId: 'end',
condition: ''
}
]
},
{
id: 'liveness',
type: FSNodeType.LIVENESS_DETECTION,
outcomes: {
livenessDetected: 'face_match',
deepfakeDetected: 'closing',
noFace: 'closing'
}
},
{
// Quick check: compare a frame from the live video against the
// photo extracted from the scanned document. No selfie capture step
// for this lightweight age-gate flow.
id: 'face_match',
type: FSNodeType.FACE_COMPARE,
sourceA: { source: 'sessionVideo' },
sourceB: { source: 'documentPhoto' },
similarityThreshold: 0.65,
outcomes: {
match: 'closing',
noMatch: 'closing',
imageUnavailable: 'closing'
}
},
{
// Generic closing for everything except the dedicated underage path,
// which has its own closing in `underage_message` above.
id: 'closing',
type: FSNodeType.CONVERSATION,
prompt: "Thank the user for completing the verification and tell them the session is now complete. Keep it to one sentence.",
doesNotRequireReply: true,
outcomes: [{ id: 'done', targetNodeId: 'end', condition: '' }]
},
{
id: 'end',
type: FSNodeType.END
}
]
})Testing Your Flows
Before deploying to production:
- Sandbox Testing: Use test API keys to verify flow logic
- Error Handling: Test all failure paths
- Performance: Monitor flow completion times
- Compliance: Ensure flows meet regulatory requirements