FaceSign

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.

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.

Basic Liveness Detection
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.

Document + Face Match
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.

Document scan with face comparison
{ 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' } },

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.

Multi-Factor Authentication Flow

Combine email and SMS verification with biometric checks.

Email + SMS + Biometric
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.

AI Conversational Flow
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.

Banking KYC Flow
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.

Healthcare Patient ID
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.

Age Verification Flow
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:

  1. Sandbox Testing: Use test API keys to verify flow logic
  2. Error Handling: Test all failure paths
  3. Performance: Monitor flow completion times
  4. Compliance: Ensure flows meet regulatory requirements

Next Steps

On this page