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:- Verify that the customer’s mobile number is genuine
- Check if the payment VPA belongs to the customer
- Screen the VPA against blocklists
- Make a risk-based decision on whether to proceed
Architecture
Implementation
Step 1: Create Verification Service
Copy
// 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
Copy
// 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
Copy
// 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
Copy
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
Copy
// 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
Copy
// 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 pollingSet 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
| Metric | Description | Target |
|---|---|---|
| Verification Latency | Time to complete verification | < 5s p95 |
| Approval Rate | % of checkouts approved | > 95% |
| False Positive Rate | Legitimate customers blocked | < 2% |
| Fraud Rate | Fraudulent orders that passed | < 0.1% |
| OTP Conversion | Users who complete OTP verification | > 80% |
