SMS Verification API: Implementation Best Practices

Drag to rearrange sections
Rich Text Content

SMS Verification API Implementation Best Practices.jpg

 

 

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

  1. Universal Accessibility: Almost everyone has a mobile phone
  2. Ease of Use: No app installation required
  3. Proven Effectiveness: Significantly reduces fraud and account takeovers
  4. 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

  1. Code Generator: Creates cryptographically secure OTPs
  2. Storage Layer: Securely stores codes with expiration
  3. Delivery Service: Sends SMS via gateway
  4. 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

rich_text    
Drag to rearrange sections
Rich Text Content
rich_text    

Page Comments