Skip to main content

E-commerce Checkout Verification

Protect your e-commerce platform from payment fraud by verifying customers before processing transactions.

Business Scenario

During checkout, you want to:
  1. Verify that the customer’s mobile number is genuine
  2. Check if the payment VPA belongs to the customer
  3. Screen the VPA against blocklists
  4. Make a risk-based decision on whether to proceed

Architecture

Implementation

Step 1: Create Verification Service

// services/checkout-verification.ts
import { TxnCheckClient } from './txncheck';

export interface VerificationInput {
  mobile: string;
  paymentVpa: string;
  orderAmount: number;
}

export interface VerificationResult {
  approved: boolean;
  riskScore: number;
  riskLevel: 'low' | 'medium' | 'high';
  customerName?: string;
  requiresOTP: boolean;
  blockedReason?: string;
}

export class CheckoutVerificationService {
  private client: TxnCheckClient;
  
  // Risk thresholds
  private readonly LOW_RISK_THRESHOLD = 30;
  private readonly HIGH_RISK_THRESHOLD = 70;
  private readonly HIGH_VALUE_ORDER = 50000; // INR

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

  async verifyCheckout(input: VerificationInput): Promise<VerificationResult> {
    const result: VerificationResult = {
      approved: false,
      riskScore: 0,
      riskLevel: 'low',
      requiresOTP: false,
    };

    try {
      // Use sync mode for real-time checkout
      const verification = await this.client.fullCheck(input.mobile, { sync: true });

      if (verification.status !== 'COMPLETED') {
        // If verification fails, require OTP for security
        result.riskScore = 100;
        result.riskLevel = 'high';
        result.requiresOTP = true;
        return result;
      }

      // Calculate risk score based on multiple factors
      result.riskScore = this.calculateRiskScore(verification, input);
      result.riskLevel = this.getRiskLevel(result.riskScore);
      
      // Extract customer name for display
      result.customerName = this.extractCustomerName(verification);
      
      // Decision logic
      if (result.riskScore >= this.HIGH_RISK_THRESHOLD) {
        result.approved = false;
        result.requiresOTP = true;
      } else if (result.riskScore >= this.LOW_RISK_THRESHOLD) {
        // Medium risk: approve but require OTP for high-value orders
        result.approved = true;
        result.requiresOTP = input.orderAmount >= this.HIGH_VALUE_ORDER;
      } else {
        // Low risk: approve without additional verification
        result.approved = true;
        result.requiresOTP = false;
      }

      return result;

    } catch (error) {
      console.error('Verification error:', error);
      // On error, require OTP as fallback
      result.riskScore = 75;
      result.riskLevel = 'high';
      result.requiresOTP = true;
      return result;
    }
  }

  private calculateRiskScore(verification: any, input: VerificationInput): number {
    let score = 0;
    const result = verification.result || {};
    
    // Check 1: VPA ownership (50 points if not linked)
    const userVpas = (result.upiByMobile?.upi || []).map((v: string) => v.toLowerCase());
    const paymentVpaLower = input.paymentVpa.toLowerCase();
    
    if (!userVpas.includes(paymentVpaLower)) {
      score += 50;
    }

    // Check 2: Blocklist status (100 points if blocklisted)
    const blocklisted = (result.vpaChargebackCheck?.blocklisted || [])
      .map((v: any) => v.vpa.toLowerCase());
    
    if (blocklisted.includes(paymentVpaLower)) {
      return 100; // Immediate rejection
    }

    // Check 3: KYC availability (20 points if no KYC)
    if (!result.kycByMobile?.pan) {
      score += 20;
    }

    // Check 4: Few linked VPAs might indicate new/suspicious account
    if (userVpas.length < 2) {
      score += 10;
    }

    return Math.min(score, 100);
  }

  private getRiskLevel(score: number): 'low' | 'medium' | 'high' {
    if (score < this.LOW_RISK_THRESHOLD) return 'low';
    if (score < this.HIGH_RISK_THRESHOLD) return 'medium';
    return 'high';
  }

  private extractCustomerName(verification: any): string | undefined {
    const result = verification.result || {};
    return result.kycByMobile?.fullName || result.upiByMobile?.name;
  }
}

Step 2: Integrate into Checkout Flow

// routes/checkout.ts
import express from 'express';
import { CheckoutVerificationService } from '../services/checkout-verification';

const router = express.Router();
const verificationService = new CheckoutVerificationService(process.env.FRAUD_BUSTER_API_KEY!);

router.post('/verify-payment', async (req, res) => {
  const { mobile, paymentVpa, orderAmount, orderId } = req.body;

  // Validate input
  if (!mobile || !paymentVpa || !orderAmount) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  try {
    const verification = await verificationService.verifyCheckout({
      mobile,
      paymentVpa,
      orderAmount,
    });

    // Log for audit
    console.log(`Order ${orderId}: Risk=${verification.riskScore}, Level=${verification.riskLevel}`);

    if (!verification.approved && !verification.requiresOTP) {
      // Hard block
      return res.status(403).json({
        success: false,
        message: 'Payment method not accepted',
        reason: verification.blockedReason || 'Security check failed',
      });
    }

    res.json({
      success: true,
      approved: verification.approved,
      customerName: verification.customerName,
      requiresOTP: verification.requiresOTP,
      riskLevel: verification.riskLevel,
    });

  } catch (error) {
    console.error('Checkout verification error:', error);
    // Fail open with OTP requirement
    res.json({
      success: true,
      approved: true,
      requiresOTP: true,
      riskLevel: 'high',
    });
  }
});

export default router;

Step 3: Frontend Integration

// components/Checkout.tsx
import React, { useState } from 'react';

interface CheckoutFormData {
  mobile: string;
  paymentVpa: string;
}

export function CheckoutPage({ orderAmount }: { orderAmount: number }) {
  const [formData, setFormData] = useState<CheckoutFormData>({ mobile: '', paymentVpa: '' });
  const [verification, setVerification] = useState<any>(null);
  const [showOTP, setShowOTP] = useState(false);
  const [loading, setLoading] = useState(false);

  const handleVerify = async () => {
    setLoading(true);
    
    try {
      const response = await fetch('/api/checkout/verify-payment', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          ...formData,
          orderAmount,
          orderId: 'ORDER-123',
        }),
      });

      const result = await response.json();
      setVerification(result);

      if (!result.success) {
        alert(result.message);
        return;
      }

      if (result.requiresOTP) {
        setShowOTP(true);
      } else {
        // Proceed to payment
        handlePayment();
      }

    } catch (error) {
      console.error('Verification failed:', error);
      // Show OTP as fallback
      setShowOTP(true);
    } finally {
      setLoading(false);
    }
  };

  const handlePayment = () => {
    // Integrate with your payment gateway
    console.log('Processing payment...');
  };

  return (
    <div className="checkout-form">
      <h2>Payment Details</h2>
      
      <input
        type="tel"
        placeholder="Mobile Number (+91...)"
        value={formData.mobile}
        onChange={(e) => setFormData({ ...formData, mobile: e.target.value })}
      />
      
      <input
        type="text"
        placeholder="UPI ID (user@bank)"
        value={formData.paymentVpa}
        onChange={(e) => setFormData({ ...formData, paymentVpa: e.target.value })}
      />

      {verification?.customerName && (
        <p className="verified-name">
Verified: {verification.customerName}
        </p>
      )}

      {showOTP ? (
        <OTPVerification onVerified={handlePayment} />
      ) : (
        <button onClick={handleVerify} disabled={loading}>
          {loading ? 'Verifying...' : 'Proceed to Pay'}
        </button>
      )}

      {verification?.riskLevel === 'high' && (
        <p className="risk-warning">
          Additional verification required for your security
        </p>
      )}
    </div>
  );
}

Edge Cases

Handle API Timeout

async verifyWithFallback(input: VerificationInput): Promise<VerificationResult> {
  try {
    // Set a shorter timeout for checkout
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout

    const result = await this.verifyCheckout(input);
    clearTimeout(timeoutId);
    return result;

  } catch (error) {
    if (error.name === 'AbortError') {
      console.warn('Verification timeout, using fallback');
    }
    
    // Fallback: approve with OTP requirement
    return {
      approved: true,
      riskScore: 50,
      riskLevel: 'medium',
      requiresOTP: true,
    };
  }
}

High-Value Order Protection

// For orders above threshold, always require additional verification
if (input.orderAmount >= 100000) { // 1 lakh INR
  result.requiresOTP = true;
  
  // Also check if this is customer's first large order
  const isFirstLargeOrder = await this.checkOrderHistory(input.mobile);
  if (isFirstLargeOrder) {
    result.riskScore += 20;
  }
}

Returning Customer Optimization

// Skip verification for trusted returning customers
async verifyWithCache(input: VerificationInput): Promise<VerificationResult> {
  const cachedVerification = await this.cache.get(`verified:${input.mobile}`);
  
  if (cachedVerification && cachedVerification.riskLevel === 'low') {
    // Still check blocklist for returning customers
    const blocklistCheck = await this.client.vpaChargebackCheck(
      [input.paymentVpa], 
      { sync: true }
    );
    
    if (blocklistCheck.result?.blocklisted?.length === 0) {
      return {
        ...cachedVerification,
        customerName: cachedVerification.customerName,
      };
    }
  }
  
  // Full verification for new or risky customers
  return this.verifyCheckout(input);
}

Production Recommendations

Use Sync Mode

For checkout flows, use async: false to get immediate results without polling

Set Timeouts

Set appropriate timeouts (10-15s) to avoid blocking checkout

Fail Open

On API failures, approve with OTP rather than blocking customers

Log Everything

Log all verifications for audit and chargeback disputes

Metrics to Track

MetricDescriptionTarget
Verification LatencyTime to complete verification< 5s p95
Approval Rate% of checkouts approved> 95%
False Positive RateLegitimate customers blocked< 2%
Fraud RateFraudulent orders that passed< 0.1%
OTP ConversionUsers who complete OTP verification> 80%