
Introduction
SMS verification has become a standard security measure for user authentication and account protection. From sign-up flows to transaction confirmations, SMS verification APIs provide a reliable way to verify user identity using their mobile phones.
This guide covers the best practices for implementing SMS verification in your applications.
Why SMS Verification?
Benefits
- Universal Accessibility: Almost everyone has a mobile phone
- Ease of Use: No app installation required
- Proven Effectiveness: Significantly reduces fraud and account takeovers
- User Familiarity: Users understand and trust SMS codes
Common Use Cases
- Account registration
- Login verification (2FA)
- Password reset
- Transaction confirmation
- Phone number verification
- Account recovery
Architecture Overview
A typical SMS verification system consists of:
User Request → Generate Code → Store Securely → Send via SMS → Verify Input
Core Components
- Code Generator: Creates cryptographically secure OTPs
- Storage Layer: Securely stores codes with expiration
- Delivery Service: Sends SMS via gateway
- Verification Engine: Validates submitted codes
Implementation Guide
Step 1: Secure Code Generation
const crypto = require('crypto');
class OTPGenerator {
static generate(length = 6) {
// Use cryptographically secure random bytes
const buffer = crypto.randomBytes(length);
let code = '';
for (let i = 0; i < length; i++) {
code += (buffer[i] % 10).toString();
}
return code;
}
static generateAlphanumeric(length = 6) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const buffer = crypto.randomBytes(length);
let code = '';
for (let i = 0; i < length; i++) {
code += chars[buffer[i] % chars.length];
}
return code;
}
}
Step 2: Secure Storage
const bcrypt = require('bcrypt');
const redis = require('redis');
class OTPStorage {
constructor() {
this.client = redis.createClient();
}
async store(phoneNumber, code, expiresInSeconds = 300) {
const hashedCode = await bcrypt.hash(code, 10);
const key = `otp:${phoneNumber}`;
await this.client.setEx(key, expiresInSeconds, JSON.stringify({
hashedCode,
attempts: 0,
createdAt: Date.now()
}));
}
async verify(phoneNumber, submittedCode) {
const key = `otp:${phoneNumber}`;
const data = await this.client.get(key);
if (!data) {
return { valid: false, error: 'CODE_NOT_FOUND' };
}
const { hashedCode, attempts } = JSON.parse(data);
if (attempts >= 3) {
await this.client.del(key);
return { valid: false, error: 'MAX_ATTEMPTS_EXCEEDED' };
}
const isValid = await bcrypt.compare(submittedCode, hashedCode);
if (!isValid) {
// Increment attempts
const updated = { ...JSON.parse(data), attempts: attempts + 1 };
await this.client.set(key, JSON.stringify(updated), { KEEPTTL: true });
return { valid: false, error: 'INVALID_CODE' };
}
// Delete used code
await this.client.del(key);
return { valid: true };
}
}
Step 3: SMS Delivery
Using a reliable SMS verification API ensures high deliverability:
import Zavudev from '@zavudev/sdk';
class SMSVerificationService {
constructor() {
this.client = new Zavudev({
apiKey: process.env.ZAVUDEV_API_KEY
});
this.otpStorage = new OTPStorage();
}
async sendVerificationCode(phoneNumber) {
// Generate code
const code = OTPGenerator.generate(6);
// Store securely
await this.otpStorage.store(phoneNumber, code);
// Send via SMS
try {
await this.zavu.messages.send({
to: phoneNumber,
text: `Your verification code is: ${code}. Valid for 5 minutes.`
});
return { success: true };
} catch (error) {
// Clean up on failure
await this.otpStorage.delete(phoneNumber);
throw error;
}
}
async verifyCode(phoneNumber, code) {
return await this.otpStorage.verify(phoneNumber, code);
}
}
Step 4: API Endpoints
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
const verificationService = new SMSVerificationService();
// Rate limiting
const requestLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5 // 5 requests per window
});
const verifyLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 10 // 10 verification attempts per window
});
// Request verification code
app.post('/api/verification/request', requestLimiter, async (req, res) => {
const { phoneNumber } = req.body;
// Validate phone number format
if (!isValidPhoneNumber(phoneNumber)) {
return res.status(400).json({ error: 'Invalid phone number' });
}
try {
await verificationService.sendVerificationCode(phoneNumber);
res.json({ success: true, message: 'Code sent' });
} catch (error) {
res.status(500).json({ error: 'Failed to send code' });
}
});
// Verify code
app.post('/api/verification/verify', verifyLimiter, async (req, res) => {
const { phoneNumber, code } = req.body;
const result = await verificationService.verifyCode(phoneNumber, code);
if (result.valid) {
res.json({ success: true });
} else {
res.status(400).json({ error: result.error });
}
});
Best Practices
1. Security Measures
// Never log verification codes
function logVerificationAttempt(phoneNumber, success) {
console.log({
phoneNumber: maskPhoneNumber(phoneNumber),
success,
timestamp: new Date().toISOString()
// Never log the actual code!
});
}
function maskPhoneNumber(phone) {
return phone.slice(0, 4) + '****' + phone.slice(-2);
}
2. User Experience
- Clear, concise messages
- Show remaining time
- Easy resend option
- Accessible alternatives
function formatVerificationMessage(code, expiresIn) {
return `Your verification code is: ${code}
This code expires in ${expiresIn} minutes.
If you didn't request this code, please ignore this message.`;
}
3. Error Handling
class VerificationError extends Error {
constructor(code, message) {
super(message);
this.code = code;
}
}
const ErrorCodes = {
RATE_LIMITED: 'Too many requests. Please wait before trying again.',
INVALID_PHONE: 'Please enter a valid phone number.',
CODE_EXPIRED: 'This code has expired. Please request a new one.',
INVALID_CODE: 'Invalid code. Please check and try again.',
MAX_ATTEMPTS: 'Too many failed attempts. Please request a new code.'
};
4. Phone Number Validation
const { parsePhoneNumber, isValidPhoneNumber } = require('libphonenumber-js');
function validateAndFormatPhone(phone, defaultCountry = 'US') {
try {
const parsed = parsePhoneNumber(phone, defaultCountry);
if (!parsed.isValid()) {
return { valid: false, error: 'Invalid phone number' };
}
return {
valid: true,
formatted: parsed.format('E.164'), // +1234567890
country: parsed.country
};
} catch (error) {
return { valid: false, error: 'Could not parse phone number' };
}
}
Advanced Patterns
Verification Flow with Resend
class EnhancedVerificationService extends SMSVerificationService {
async requestCode(phoneNumber, options = {}) {
const { isResend = false } = options;
// Check cooldown for resends
if (isResend) {
const cooldown = await this.checkCooldown(phoneNumber);
if (cooldown.active) {
return {
success: false,
error: 'COOLDOWN_ACTIVE',
retryAfter: cooldown.remainingSeconds
};
}
}
// Check daily limit
const dailyCount = await this.getDailyCount(phoneNumber);
if (dailyCount >= 10) {
return { success: false, error: 'DAILY_LIMIT_EXCEEDED' };
}
await this.sendVerificationCode(phoneNumber);
await this.setCooldown(phoneNumber, 60); // 60 second cooldown
await this.incrementDailyCount(phoneNumber);
return { success: true };
}
}
Fallback to Voice Call
async function sendWithFallback(phoneNumber) {
try {
// Try SMS first
await sendSMSCode(phoneNumber);
return { method: 'sms' };
} catch (smsError) {
if (smsError.code === 'UNDELIVERABLE') {
// Fallback to voice call
await sendVoiceCode(phoneNumber);
return { method: 'voice' };
}
throw smsError;
}
}
Monitoring and Analytics
Track key metrics:
const metrics = {
requestsSent: 0,
successfulVerifications: 0,
failedVerifications: 0,
expiredCodes: 0,
averageVerificationTime: 0
};
function trackVerification(event) {
switch (event.type) {
case 'SENT':
metrics.requestsSent++;
break;
case 'VERIFIED':
metrics.successfulVerifications++;
break;
case 'FAILED':
metrics.failedVerifications++;
break;
case 'EXPIRED':
metrics.expiredCodes++;
break;
}
// Send to analytics service
analytics.track('verification_event', event);
}
Conclusion
Implementing SMS verification correctly requires attention to security, user experience, and reliability. By following the best practices outlined in this guide, you can build a verification system that protects your users while providing a smooth experience.
For production deployments, using a specialized SMS verification service ensures high deliverability, global coverage, and the reliability your application needs.
Resources