> ## Documentation Index
> Fetch the complete documentation index at: https://docs.facesign.run/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

<Note>
  **Camera buffer requirement**

  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](/docs/guides/conversation-rules) for the full rule.
</Note>

## 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.

```typescript title="Basic Liveness Detection" wrap theme={null}
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.

```typescript title="Document + Face Match" wrap theme={null}
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.

```javascript title="Document scan with face comparison" wrap theme={null}
{ 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' } },
```

<Note>
  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.
</Note>

## 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.

<Warning>
  `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.
</Warning>

### Multi-Factor Authentication Flow

Combine email and SMS verification with biometric checks.

```typescript title="Email + SMS + Biometric" wrap theme={null}
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.

```typescript title="AI Conversational Flow" wrap theme={null}
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.

```typescript title="Banking KYC Flow" wrap theme={null}
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.

```typescript title="Healthcare Patient ID" wrap theme={null}
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.

```typescript title="Age Verification Flow" wrap theme={null}
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

<CardGroup cols={2}>
  <Card title="Node Reference" href="/docs/reference/node-reference">
    Complete field reference for all node types
  </Card>

  <Card title="Languages & Localization" href="/docs/guides/multilingual">
    Set up multilingual flows with proper prompt style
  </Card>

  <Card title="Production Checklist" href="/docs/guides/production-checklist">
    Everything to verify before going live
  </Card>
</CardGroup>
