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

# Customer Onboarding

> KYC verification during user registration

# Customer Onboarding Verification

Streamline your KYC (Know Your Customer) process by verifying customer identity during registration using just their mobile number.

## Business Scenario

During user registration, you want to:

1. Verify the customer's identity without manual document upload
2. Pre-fill profile information (name, DOB) for better UX
3. Check for any fraud signals before account creation
4. Comply with regulatory requirements

## Architecture

```mermaid theme={null}
flowchart TD
    A[User enters mobile] --> B[Send OTP]
    B --> C{OTP verified?}
    C -->|No| B
    C -->|Yes| D[Call TxnCheck API]
    D --> E{KYC data found?}
    E -->|Yes| F[Pre-fill profile]
    E -->|No| G[Manual entry]
    F --> H[Risk assessment]
    G --> H
    H --> I{Risk level?}
    I -->|Low| J[Account created]
    I -->|Medium| K[Limited account]
    I -->|High| L[Reject/Manual review]
```

## Implementation

### Step 1: Create Onboarding Service

```typescript theme={null}
// services/onboarding-verification.ts
import { TxnCheckClient } from './fraud-buster';

export interface OnboardingInput {
  mobile: string;
  userProvidedName?: string;
  userProvidedDOB?: string;
}

export interface OnboardingResult {
  verified: boolean;
  kycStatus: 'verified' | 'partial' | 'not_found' | 'error';
  prefillData?: {
    fullName: string;
    maskedPan: string;
    maskedAadhaar: string;
    dob: string;
  };
  linkedVPAs: string[];
  riskFlags: string[];
  accountType: 'full' | 'limited' | 'rejected';
  requiresManualReview: boolean;
}

export class OnboardingService {
  private client: TxnCheckClient;

  constructor(apiKey: string) {
    this.client = new TxnCheckClient({ apiKey });
  }

  async verifyNewCustomer(input: OnboardingInput): Promise<OnboardingResult> {
    const result: OnboardingResult = {
      verified: false,
      kycStatus: 'not_found',
      linkedVPAs: [],
      riskFlags: [],
      accountType: 'limited',
      requiresManualReview: false,
    };

    try {
      // Perform full check to get all available data
      const verification = await this.client.fullCheck(input.mobile, { sync: true });

      if (verification.status === 'FAILED') {
        result.kycStatus = 'error';
        result.accountType = 'limited';
        return result;
      }

      const data = verification.result || {};
      const kycData = data.kycByMobile || {};
      const upiData = data.upiByMobile || {};
      const blocklist = data.vpaChargebackCheck || {};

      // Extract KYC data
      if (kycData.fullName && kycData.pan) {
        result.kycStatus = 'verified';
        result.verified = true;
        result.prefillData = {
          fullName: kycData.fullName,
          maskedPan: this.maskPAN(kycData.pan),
          maskedAadhaar: kycData.maskedAadhaar || '',
          dob: kycData.dob || '',
        };
      } else if (upiData.name) {
        result.kycStatus = 'partial';
        result.prefillData = {
          fullName: upiData.name,
          maskedPan: '',
          maskedAadhaar: '',
          dob: '',
        };
      }

      // Extract linked VPAs
      result.linkedVPAs = upiData.upi || [];

      // Risk assessment
      const riskFlags = this.assessRisks(data, input);
      result.riskFlags = riskFlags;

      // Determine account type based on risks
      result.accountType = this.determineAccountType(result, riskFlags);
      result.requiresManualReview = riskFlags.includes('BLOCKLISTED_VPA') || 
                                     riskFlags.includes('NAME_MISMATCH');

      return result;

    } catch (error) {
      console.error('Onboarding verification error:', error);
      result.kycStatus = 'error';
      result.accountType = 'limited';
      return result;
    }
  }

  private assessRisks(data: any, input: OnboardingInput): string[] {
    const flags: string[] = [];
    const kycData = data.kycByMobile || {};
    const blocklist = data.vpaChargebackCheck || {};

    // Check for blocklisted VPAs
    if (blocklist.blocklisted?.length > 0) {
      flags.push('BLOCKLISTED_VPA');
    }

    // Check name mismatch (if user provided name)
    if (input.userProvidedName && kycData.fullName) {
      const similarity = this.calculateNameSimilarity(
        input.userProvidedName.toLowerCase(),
        kycData.fullName.toLowerCase()
      );
      if (similarity < 0.7) {
        flags.push('NAME_MISMATCH');
      }
    }

    // Check DOB mismatch
    if (input.userProvidedDOB && kycData.dob) {
      if (input.userProvidedDOB !== kycData.dob) {
        flags.push('DOB_MISMATCH');
      }
    }

    // No linked VPAs might indicate synthetic identity
    if ((data.upiByMobile?.upi || []).length === 0) {
      flags.push('NO_UPI_HISTORY');
    }

    return flags;
  }

  private determineAccountType(
    result: OnboardingResult, 
    riskFlags: string[]
  ): 'full' | 'limited' | 'rejected' {
    // Reject if blocklisted VPA found
    if (riskFlags.includes('BLOCKLISTED_VPA')) {
      return 'rejected';
    }

    // Full access if KYC verified and no critical risks
    if (result.kycStatus === 'verified' && 
        !riskFlags.includes('NAME_MISMATCH') && 
        !riskFlags.includes('DOB_MISMATCH')) {
      return 'full';
    }

    // Limited access otherwise
    return 'limited';
  }

  private maskPAN(pan: string): string {
    if (pan.length !== 10) return pan;
    return pan.slice(0, 2) + '****' + pan.slice(-4);
  }

  private calculateNameSimilarity(a: string, b: string): number {
    // Simple Levenshtein-based similarity
    const distance = this.levenshteinDistance(a, b);
    const maxLength = Math.max(a.length, b.length);
    return 1 - distance / maxLength;
  }

  private levenshteinDistance(a: string, b: string): number {
    const matrix: number[][] = [];
    for (let i = 0; i <= b.length; i++) {
      matrix[i] = [i];
    }
    for (let j = 0; j <= a.length; j++) {
      matrix[0][j] = j;
    }
    for (let i = 1; i <= b.length; i++) {
      for (let j = 1; j <= a.length; j++) {
        if (b.charAt(i - 1) === a.charAt(j - 1)) {
          matrix[i][j] = matrix[i - 1][j - 1];
        } else {
          matrix[i][j] = Math.min(
            matrix[i - 1][j - 1] + 1,
            matrix[i][j - 1] + 1,
            matrix[i - 1][j] + 1
          );
        }
      }
    }
    return matrix[b.length][a.length];
  }
}
```

### Step 2: Registration API Endpoint

```typescript theme={null}
// routes/registration.ts
import express from 'express';
import { OnboardingService } from '../services/onboarding-verification';

const router = express.Router();
const onboardingService = new OnboardingService(process.env.FRAUD_BUSTER_API_KEY!);

// Step 1: Verify mobile and get KYC data
router.post('/verify-mobile', async (req, res) => {
  const { mobile, otpToken } = req.body;

  // Verify OTP first
  const otpValid = await verifyOTP(mobile, otpToken);
  if (!otpValid) {
    return res.status(401).json({ error: 'Invalid OTP' });
  }

  try {
    const verification = await onboardingService.verifyNewCustomer({ mobile });

    // Don't expose internal flags to client
    res.json({
      success: true,
      kycStatus: verification.kycStatus,
      prefillData: verification.prefillData,
      accountType: verification.accountType,
      linkedVPAs: verification.linkedVPAs.slice(0, 3), // Limit exposure
    });

  } catch (error) {
    console.error('Mobile verification error:', error);
    res.status(500).json({ error: 'Verification service unavailable' });
  }
});

// Step 2: Complete registration
router.post('/complete', async (req, res) => {
  const { mobile, name, dob, email, acceptedTerms } = req.body;

  // Re-verify with user-provided data
  const verification = await onboardingService.verifyNewCustomer({
    mobile,
    userProvidedName: name,
    userProvidedDOB: dob,
  });

  if (verification.accountType === 'rejected') {
    return res.status(403).json({
      error: 'Unable to create account',
      message: 'Please contact support for assistance',
    });
  }

  // Create user account
  const user = await createUser({
    mobile,
    name: verification.prefillData?.fullName || name,
    dob: verification.prefillData?.dob || dob,
    email,
    kycVerified: verification.kycStatus === 'verified',
    accountType: verification.accountType,
    linkedVPAs: verification.linkedVPAs,
  });

  // Flag for manual review if needed
  if (verification.requiresManualReview) {
    await flagForReview(user.id, verification.riskFlags);
  }

  res.json({
    success: true,
    userId: user.id,
    accountType: user.accountType,
    kycVerified: user.kycVerified,
  });
});

export default router;
```

### Step 3: Frontend Registration Flow

```typescript theme={null}
// components/Registration.tsx
import React, { useState } from 'react';

export function RegistrationFlow() {
  const [step, setStep] = useState<'mobile' | 'otp' | 'profile' | 'complete'>('mobile');
  const [mobile, setMobile] = useState('');
  const [kycData, setKycData] = useState<any>(null);
  const [profileData, setProfileData] = useState({ name: '', dob: '', email: '' });

  const handleMobileSubmit = async () => {
    // Send OTP
    await sendOTP(mobile);
    setStep('otp');
  };

  const handleOTPVerify = async (otp: string) => {
    const response = await fetch('/api/registration/verify-mobile', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ mobile, otpToken: otp }),
    });

    const result = await response.json();
    
    if (result.success) {
      setKycData(result);
      
      // Pre-fill profile if KYC data available
      if (result.prefillData) {
        setProfileData({
          name: result.prefillData.fullName,
          dob: result.prefillData.dob,
          email: '',
        });
      }
      
      setStep('profile');
    }
  };

  const handleProfileSubmit = async () => {
    const response = await fetch('/api/registration/complete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        mobile,
        ...profileData,
        acceptedTerms: true,
      }),
    });

    const result = await response.json();
    
    if (result.success) {
      setStep('complete');
    }
  };

  return (
    <div className="registration-flow">
      {step === 'mobile' && (
        <div>
          <h2>Create Account</h2>
          <input
            type="tel"
            placeholder="Mobile Number (+91...)"
            value={mobile}
            onChange={(e) => setMobile(e.target.value)}
          />
          <button onClick={handleMobileSubmit}>Send OTP</button>
        </div>
      )}

      {step === 'otp' && (
        <OTPInput onVerify={handleOTPVerify} onResend={() => sendOTP(mobile)} />
      )}

      {step === 'profile' && (
        <div>
          <h2>Complete Your Profile</h2>
          
          {kycData?.kycStatus === 'verified' && (
            <div className="kyc-badge">
              ✓ Identity Verified
              <small>Your details have been verified</small>
            </div>
          )}

          <input
            type="text"
            placeholder="Full Name"
            value={profileData.name}
            onChange={(e) => setProfileData({ ...profileData, name: e.target.value })}
            disabled={kycData?.kycStatus === 'verified'}
          />

          <input
            type="date"
            placeholder="Date of Birth"
            value={profileData.dob}
            onChange={(e) => setProfileData({ ...profileData, dob: e.target.value })}
            disabled={kycData?.kycStatus === 'verified'}
          />

          <input
            type="email"
            placeholder="Email"
            value={profileData.email}
            onChange={(e) => setProfileData({ ...profileData, email: e.target.value })}
          />

          {kycData?.accountType === 'limited' && (
            <div className="account-notice">
              <p>Your account will have limited features until additional verification is completed.</p>
            </div>
          )}

          <button onClick={handleProfileSubmit}>Create Account</button>
        </div>
      )}

      {step === 'complete' && (
        <div className="success">
          <h2>Welcome!</h2>
          <p>Your account has been created successfully.</p>
        </div>
      )}
    </div>
  );
}
```

## Account Types & Limits

Define different account tiers based on verification status:

| Account Type | KYC Status        | Transaction Limit | Features             |
| ------------ | ----------------- | ----------------- | -------------------- |
| Full         | Verified          | ₹1,00,000/day     | All features enabled |
| Limited      | Partial/Not Found | ₹10,000/day       | Basic features only  |
| Rejected     | Blocklisted       | N/A               | Account not created  |

## Edge Cases

### Handle Partial KYC Data

```typescript theme={null}
// If KYC lookup returns partial data, request missing fields
if (result.kycStatus === 'partial') {
  // Name from UPI data but no PAN/Aadhaar
  return {
    ...result,
    requiredFields: ['pan', 'dob'],
    message: 'Please provide additional details for full verification',
  };
}
```

### Multiple Mobile Numbers

```typescript theme={null}
// Some users have multiple mobiles - allow linking
router.post('/link-mobile', async (req, res) => {
  const { userId, newMobile, otpToken } = req.body;
  
  // Verify the new mobile
  const verification = await onboardingService.verifyNewCustomer({ mobile: newMobile });
  
  // Check if name matches existing account
  const existingUser = await getUserById(userId);
  if (verification.prefillData?.fullName !== existingUser.name) {
    return res.status(400).json({
      error: 'Name mismatch',
      message: 'The mobile number appears to belong to a different person',
    });
  }
  
  // Link mobile to account
  await linkMobileToUser(userId, newMobile);
  res.json({ success: true });
});
```

## Compliance Considerations

<Warning>
  KYC data (PAN, Aadhaar) must be handled in compliance with data protection regulations:

  * Store only masked/hashed versions
  * Log access for audit purposes
  * Implement data retention policies
  * Encrypt data at rest and in transit
</Warning>

### Data Minimization

```typescript theme={null}
// Only store what's necessary
const userRecord = {
  mobile: input.mobile,
  name: verification.prefillData?.fullName,
  // Store hash of PAN, not actual PAN
  panHash: hashPAN(verification.prefillData?.maskedPan),
  kycVerified: verification.verified,
  verifiedAt: new Date(),
};
```

## Production Recommendations

<CardGroup cols={2}>
  <Card title="Rate Limit" icon="gauge">
    Limit KYC lookups per IP/session to prevent enumeration attacks
  </Card>

  <Card title="Audit Logging" icon="file-lines">
    Log all KYC lookups with user consent timestamp
  </Card>

  <Card title="Fallback Flow" icon="route">
    Have manual document upload as fallback for failed lookups
  </Card>

  <Card title="Data Retention" icon="clock">
    Define and enforce data retention policies for KYC data
  </Card>
</CardGroup>

## Related Guides

<CardGroup cols={2}>
  <Card title="E-commerce Checkout" icon="shopping-cart" href="/use-cases/ecommerce-checkout">
    Payment verification flow
  </Card>

  <Card title="Data Handling" icon="database" href="/security/data-handling">
    Handling PII securely
  </Card>
</CardGroup>
