Use Case
WhatsApp OTP Authentication: Free SMS Alternative for 2025
Replace SMS OTP with WhatsApp two-factor authentication. Lower costs (90% savings), higher delivery rates, better UX. Full working code in Node.js, database schema, and production security practices included.
WhatsApp OTP Authentication: Free SMS Alternative for 2025
Meta Description: Replace SMS OTP with WhatsApp two-factor authentication. Lower costs, higher delivery rates, better UX. Complete guide with code examples.Introduction
SMS OTP authentication is yesterday's problem.
Your users expect WhatsApp. Your SMS provider charges $0.01–$0.05 per message. Delivery times vary. And every user who doesn't have SMS forwarding enabled might bounce out of your registration flow.
Enter WhatsApp OTP authentication—the modern replacement that's:
- Faster: Delivered in seconds (vs. SMS delays)
- Cheaper: ~90% less than SMS services
- Better UX: Notifications appear on the same app users live in
- Lower bounce: Nobody ignores WhatsApp notifications
Why WhatsApp OTP Beats SMS (Numbers That Matter)
Cost Comparison
| Provider | Cost/OTP | Annual (1M users) | |----------|---------|-----------------| | Twilio SMS | $0.0075 | $7,500 | | AWS SNS | $0.00645 | $6,450 | | MessageBird SMS | $0.01 | $10,000 | | WhatsApp API | $0.001–$0.005 | $1,000–$5,000 |
You save $2,000–$9,000 per million users annually.Delivery & User Experience
| Metric | SMS | WhatsApp | |--------|-----|----------| | Avg. Delivery Time | 3–8 seconds | 1–2 seconds | | Delivery Rate | 94–98% | 99%+ | | User Engagement | See notification in messages app | Pop-up inside WhatsApp | | Copy-Paste UX | Manual (error-prone) | 1-tap autofill |
How WhatsApp OTP Works
The Flow
User enters email/phone
↓
Backend generates OTP (6 digits)
↓
Backend sends OTP via WhatsApp
↓
User receives in WhatsApp (1-2s)
↓
User enters OTP or taps autofill
↓
Backend verifies & issues session token
↓
User logged inWhy it's faster than SMS: WhatsApp push notifications wake the app instantly. SMS waits for the phone's messaging service. Why it's cheaper: The WhatsApp API on RapidAPI charges per message, not per SMS service provider markup.Step 1: Project Setup
Prerequisites
- Node.js 16+
- A RapidAPI account (free)
- One test WhatsApp account (your personal account)
- Basic Express.js knowledge
Initialize Project
bash
mkdir whatsapp-otp-auth
cd whatsapp-otp-auth
npm init -y
npm install express axios dotenv bcryptjs jsonwebtoken cors sqlite3
npm install --save-dev nodemonCreate structure
bash
mkdir -p src/{routes,controllers,models,utils,config}
touch src/index.js
touch .envUpdate package.json scripts
json
{
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js"
}
}Environment file (.env)
PORT=3000
NODE_ENV=developmentRAPIDAPI_KEY=your_api_key_here
RAPIDAPI_HOST=whatsapp-messaging-bot.p.rapidapi.com
WHATSAPP_SESSION=default
JWT_SECRET=your-super-secret-key-change-in-prod
OTP_EXPIRY_MINUTES=10
Step 2: Database Schema (SQLite)
Create src/config/database.js:
javascript
const sqlite3 = require('sqlite3').verbose();
const path = require('path');const db = new sqlite3.Database(
path.join(__dirname, '../../auth.db'),
(err) => {
if (err) console.error('DB connection error:', err);
else console.log('✅ SQLite database connected');
}
);
// Initialize tables
db.serialize(() => {
// Users table
db.run(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
phone_number TEXT UNIQUE NOT NULL,
password_hash TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME
)
);
// OTP table
db.run(
CREATE TABLE IF NOT EXISTS otps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
phone_number TEXT NOT NULL,
otp_code TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
verified INTEGER DEFAULT 0,
attempt_count INTEGER DEFAULT 0
)
);
// Sessions table
db.run(
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
)
);
console.log('✅ Database tables initialized');
});
module.exports = db;
Step 3: WhatsApp Service Integration
Create src/utils/whatsappService.js:
javascript
const axios = require('axios');class WhatsAppOTPService {
constructor(apiKey, apiHost, sessionName) {
this.apiKey = apiKey;
this.apiHost = apiHost;
this.sessionName = sessionName;
this.baseUrl = https://${apiHost};
}
async sendOTP(phoneNumber, otpCode) {
try {
const message = 🔐 Your verification code is: ${otpCode}\n\nExpires in 10 minutes. Don't share this code.;
const response = await axios.post(
${this.baseUrl}/v1/sendText,
{
chatId: phoneNumber, // Pure phone number
text: message,
session: this.sessionName
},
{
headers: {
'x-rapidapi-key': this.apiKey,
'x-rapidapi-host': this.apiHost,
},
}
);
console.log(✅ OTP sent to ${phoneNumber});
return { success: true, messageId: response.data.id };
} catch (error) {
console.error(❌ Failed to send OTP to ${phoneNumber}:, error.message);
throw new Error(WhatsApp service error: ${error.message});
}
}
async sendVerificationMessage(phoneNumber, userName) {
try {
const message = 👋 Welcome, ${userName}! Your account is now verified on WhatsApp.;
await axios.post(
${this.baseUrl}/v1/sendText,
{
chatId: phoneNumber,
text: message,
session: this.sessionName
},
{
headers: {
'x-rapidapi-key': this.apiKey,
'x-rapidapi-host': this.apiHost,
},
}
);
return { success: true };
} catch (error) {
console.error('Error sending verification message:', error.message);
throw error;
}
}
}
module.exports = WhatsAppOTPService;
Step 4: OTP Controller Logic
Create src/controllers/authController.js:
javascript
const crypto = require('crypto');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const db = require('../config/database');
const WhatsAppOTPService = require('../utils/whatsappService');const whatsapp = new WhatsAppOTPService(
process.env.RAPIDAPI_KEY,
process.env.RAPIDAPI_HOST,
process.env.WHATSAPP_SESSION
);
// Helper to run DB queries
function dbRun(query, params) {
return new Promise((resolve, reject) => {
db.run(query, params, function(err) {
if (err) reject(err);
else resolve(this);
});
});
}
function dbGet(query, params) {
return new Promise((resolve, reject) => {
db.get(query, params, (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
}
// Generate 6-digit OTP
function generateOTP() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// Generate JWT token
function createToken(userId) {
return jwt.sign(
{ userId, timestamp: Date.now() },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
}
// Step 1: User requests OTP
exports.requestOTP = async (req, res) => {
try {
const { phoneNumber } = req.body;
if (!phoneNumber || !/^\d{10,15}$/.test(phoneNumber)) {
return res.status(400).json({ error: 'Invalid phone number format' });
}
// Generate OTP
const otpCode = generateOTP();
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
// Save OTP to database
await dbRun(
'INSERT INTO otps (phone_number, otp_code, expires_at) VALUES (?, ?, ?)',
[phoneNumber, otpCode, expiresAt.toISOString()]
);
// Send via WhatsApp
await whatsapp.sendOTP(phoneNumber, otpCode);
res.json({
success: true,
message: 'OTP sent to WhatsApp',
phoneNumber,
// In production, DO NOT return the OTP. This is for testing only.
...(process.env.NODE_ENV === 'development' && { otpCode }),
});
} catch (error) {
console.error('Error requesting OTP:', error);
res.status(500).json({ error: error.message });
}
};
// Step 2: User verifies OTP
exports.verifyOTP = async (req, res) => {
try {
const { phoneNumber, otpCode, email, password } = req.body;
if (!phoneNumber || !otpCode) {
return res.status(400).json({ error: 'Phone number and OTP required' });
}
// Find valid OTP
const otpRecord = await dbGet(
SELECT * FROM otps
WHERE phone_number = ?
AND otp_code = ?
AND expires_at > datetime('now')
AND verified = 0
ORDER BY created_at DESC
LIMIT 1,
[phoneNumber, otpCode]
);
if (!otpRecord) {
return res.status(401).json({
error: 'Invalid or expired OTP. Request a new one.',
});
}
// Mark OTP as verified
await dbRun(
'UPDATE otps SET verified = 1 WHERE id = ?',
[otpRecord.id]
);
// Check if user exists
let user = await dbGet(
'SELECT id FROM users WHERE phone_number = ?',
[phoneNumber]
);
// If not, create user
if (!user) {
if (!email || !password) {
return res.status(400).json({
error: 'Email and password required for new users',
});
}
const passwordHash = await bcrypt.hash(password, 10);
const result = await dbRun(
'INSERT INTO users (email, phone_number, password_hash) VALUES (?, ?, ?)',
[email, phoneNumber, passwordHash]
);
user = { id: result.lastID };
// Send welcome message
try {
await whatsapp.sendVerificationMessage(phoneNumber, email.split('@')[0]);
} catch (err) {
console.warn('Welcome message failed:', err.message);
}
}
// Create session
const token = createToken(user.id);
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
await dbRun(
'INSERT INTO sessions (user_id, token, expires_at) VALUES (?, ?, ?)',
[user.id, token, expiresAt.toISOString()]
);
// Update last login
await dbRun(
'UPDATE users SET last_login = datetime("now") WHERE id = ?',
[user.id]
);
res.json({
success: true,
message: 'Authentication successful',
token,
user: { id: user.id, phoneNumber },
expiresIn: '7d',
});
} catch (error) {
console.error('Error verifying OTP:', error);
res.status(500).json({ error: error.message });
}
};
// Step 3: Validate token (for protected routes)
exports.validateToken = async (req, res, next) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Check session in database
const session = await dbGet(
'SELECT * FROM sessions WHERE token = ? AND expires_at > datetime("now")',
[token]
);
if (!session) {
return res.status(401).json({ error: 'Session expired or invalid' });
}
req.user = { id: decoded.userId };
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
Step 5: API Routes
Create src/routes/auth.js:
javascript
const express = require('express');
const authController = require('../controllers/authController');const router = express.Router();
// Public routes
router.post('/request-otp', authController.requestOTP);
router.post('/verify-otp', authController.verifyOTP);
// Protected route (example)
router.get('/me', authController.validateToken, (req, res) => {
res.json({ message: 'Protected content', userId: req.user.id });
});
module.exports = router;
Step 6: Express Server Setup
Create src/index.js:
javascript
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const authRoutes = require('./routes/auth');const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/auth', authRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(🚀 Server running at http://localhost:${PORT});
console.log(📖 API docs: http://localhost:${PORT}/health);
});
Testing the Full Flow
1. Request OTP
bash
curl -X POST http://localhost:3000/api/auth/request-otp \
-H "Content-Type: application/json" \
-d '{"phoneNumber": "14155552671"}'Response (development mode shows OTP):
json
{
"success": true,
"message": "OTP sent to WhatsApp",
"phoneNumber": "14155552671",
"otpCode": "123456"
}Check your WhatsApp—you'll get the OTP instantly.
2. Verify OTP
bash
curl -X POST http://localhost:3000/api/auth/verify-otp \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "14155552671",
"otpCode": "123456",
"email": "user@example.com",
"password": "securePassword123"
}'Response:
json
{
"success": true,
"message": "Authentication successful",
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {"id": 1, "phoneNumber": "14155552671"},
"expiresIn": "7d"
}3. Use the Token
bash
curl -X GET http://localhost:3000/api/auth/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Security Best Practices
1. Rate Limiting on OTP Requests
Prevent brute force attacks:javascript
const rateLimit = require('express-rate-limit');const otpLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 3, // 3 OTP requests per window
message: 'Too many OTP requests. Try again later.',
});
router.post('/request-otp', otpLimiter, authController.requestOTP);
2. Limit OTP Verification Attempts
javascript
// In authController.verifyOTP:
const maxAttempts = 5;
if (otpRecord.attempt_count >= maxAttempts) {
return res.status(429).json({ error: 'Too many attempts. Request a new OTP.' });
}// Increment fail attempt
await dbRun(
'UPDATE otps SET attempt_count = attempt_count + 1 WHERE id = ?',
[otpRecord.id]
);
3. Use HTTPS in Production
Always use TLS for token transmission.javascript
// In production, force HTTPS
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(https://${req.headers.host}${req.url});
}
next();
});
}4. Never Store OTP in Logs
Use structured logging without sensitive data:javascript
logger.info('OTP sent', { phoneNumber, timestamp: new Date() });
// ❌ NOT: logger.info(OTP ${otpCode} sent to ${phoneNumber});Frontend Integration (React Example)
javascript
import { useState } from 'react';export default function WhatsAppOTPAuth() {
const [step, setStep] = useState('phone'); // 'phone' or 'otp'
const [phoneNumber, setPhoneNumber] = useState('');
const [otpCode, setOtpCode] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [token, setToken] = useState(null);
const handleRequestOTP = async (e) => {
e.preventDefault();
setLoading(true);
try {
const res = await fetch('/api/auth/request-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phoneNumber }),
});
const data = await res.json();
if (data.success) {
setStep('otp');
alert('Check your WhatsApp for the OTP!');
}
} catch (error) {
alert('Error: ' + error.message);
}
setLoading(false);
};
const handleVerifyOTP = async (e) => {
e.preventDefault();
setLoading(true);
try {
const res = await fetch('/api/auth/verify-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phoneNumber, otpCode, email, password }),
});
const data = await res.json();
if (data.success) {
setToken(data.token);
localStorage.setItem('authToken', data.token);
alert('✅ Logged in!');
// Redirect to dashboard
}
} catch (error) {
alert('Error: ' + error.message);
}
setLoading(false);
};
if (token) return
✅ Authenticated! Token: {token.substring(0, 20)}...; if (step === 'phone') {
return (
);
} return (
);
}Cost Savings at Scale
Example: SaaS with 100,000 Monthly Active Users
SMS OTP (Twilio):- Cost per OTP: $0.0075
- Assuming 1.5 OTP per signup: $1,125/month = $13,500/year
- Cost per OTP: $0.003
- Assuming 1.5 OTP per signup: $450/month = $5,400/year
- SMS: $135,000/year
- WhatsApp: $54,000/year
- Savings: $81,000/year
Monitoring & Troubleshooting
Track OTP Success Rate
javascript
const otpStats = {};// In verifyOTP:
const success = otpRecord ? true : false;
otpStats.verified = (otpStats.verified || 0) + (success ? 1 : 0);
otpStats.failed = (otpStats.failed || 0) + (success ? 0 : 1);
otpStats.rate = (otpStats.verified / (otpStats.verified + otpStats.failed) * 100).toFixed(2) + '%';
console.log('OTP Stats:', otpStats);
Common Issues
| Problem | Fix |
|---------|-----|
| UserJWT verification fails | Token expired. User needs to login again. |
| OTP not arriving | User's WhatsApp not connected to session. Re-scan QR code. |
| Duplicate user error | User already exists. Handle edge case in verifyOTP. |
| Rate limit hit | Check RapidAPI plan limits. Upgrade if needed. |
Migration from SMS to WhatsApp
If you currently use SMS:
1. Offer WhatsApp OTP alongside SMS for 30 days 2. Track adoption – most users will prefer WhatsApp 3. Deprecate SMS once 80%+ use WhatsApp 4. Celebrate savings 🎉
Next Steps
- Send WhatsApp Messages with Node.js - Master the basics of WhatsApp API integration
- Build a WhatsApp Chatbot - Add conversational flows and AI-powered replies
- Best WhatsApp API for Startups - Compare providers and choose the right one
- WhatsApp Appointment Reminders - Reduce no-shows with automated reminders
- WhatsApp API vs Twilio - Compare pricing and features
Key Takeaways
1. WhatsApp OTP is 60% cheaper than SMS and delivers faster 2. Better UX – users already live in WhatsApp 3. Higher security – users can't forward codes like SMS 4. Easy migration – run parallel to SMS, then deprecate 5. RapidAPI makes it accessible – no approval forms, code in minutes
Ready to Implement?
Try Free on RapidAPI → Subscribe → Get API key → Copy the code above → Done.Your users will thank you. Your CFO will too.
Ready to Get Started?
Try the WhatsApp API free on RapidAPI with no credit card required.
Try Free on RapidAPI