Build OTP/2FA with MessageBird Verify API: Complete Vite + React/Vue Guide
Implement secure SMS-based OTP two-factor authentication using MessageBird Verify API with Node.js backend and Vite (React/Vue) frontend. Complete tutorial with code generation, verification, and security best practices.
Build a production-ready SMS-based One-Time Password (OTP) two-factor authentication (2FA) system using MessageBird's Verify API with a Node.js/Express backend and modern Vite-powered frontend (React or Vue). Implement secure code generation, verification flows, and handle edge cases for real-world applications.
What is OTP/2FA and Why MessageBird?
One-Time Password (OTP) authentication sends a temporary numeric code (typically 4–10 digits) to a user's mobile phone via SMS. Users must enter this code within a limited timeframe (usually 30 seconds to 2 days) to verify their identity. Two-Factor Authentication (2FA) adds this second verification layer beyond username/password, significantly reducing unauthorized access even if passwords are compromised (NIST SP 800-63B recommends 2FA for sensitive applications as of 2023).
MessageBird's Verify API simplifies OTP implementation by:
Automatically generating secure random tokens (6–10 digits, configurable)
Handling message delivery via SMS, voice (TTS), or email
Providing status tracking (sent, verified, expired, failed)
According to MessageBird's official Verify API documentation, the service handles token generation server-side, eliminating client-side security risks and simplifying implementation compared to manual OTP solutions.
Project Overview and Goals
Goal: Build a full-stack OTP/2FA system with:
Backend (Node.js/Express): Three API endpoints for generating OTP codes (/api/otp/generate), verifying codes (/api/otp/verify), and viewing verification status (/api/otp/status)
Frontend (Vite + React or Vue): Phone number input form, OTP code entry interface with auto-focus, and verification status display
Problem Solved: Adds robust two-factor authentication to protect user accounts, secure transactions, verify phone numbers during registration, and meet compliance requirements for financial services, healthcare, or e-commerce applications.
Technologies Used:
Node.js: JavaScript runtime for backend server
Express: Web application framework for API routing
messagebird npm package: Official MessageBird SDK for Node.js (v4.0.1 as of January 2022, Apache-2.0 license, GitHub repository)
dotenv: Environment variable management
Vite: Next-generation frontend build tool (faster than webpack/CRA)
React or Vue 3: Modern frontend framework for UI components
axios: HTTP client for API requests from frontend
express-rate-limit: Protection against brute-force attacks
System Architecture:
The complete OTP flow involves both frontend and backend coordination:
sequenceDiagram
participant User
participant Frontend as Vite Frontend<br/>(React/Vue)
participant Backend as Node.js/Express API
participant MessageBird as MessageBird Verify API
participant Phone as User's Phone
User->>Frontend: 1. Enter phone number
Frontend->>Backend: 2. POST /api/otp/generate {phone}
Backend->>MessageBird: 3. Create Verify object
MessageBird->>Phone: 4. Send SMS with OTP code
MessageBird-->>Backend: 5. Return verify ID, status
Backend-->>Frontend: 6. Return {verifyId, expiry}
Frontend->>User: 7. Show OTP input form
User->>Frontend: 8. Enter OTP code
Frontend->>Backend: 9. POST /api/otp/verify {verifyId, token}
Backend->>MessageBird: 10. Verify token
MessageBird-->>Backend: 11. Return status (verified/failed)
Backend-->>Frontend: 12. Return success/error
Frontend->>User: 13. Show result (access granted/denied)
Prerequisites:
Node.js and npm: Version 14.x or higher (nodejs.org)
MessageBird Account: Free account with API key from MessageBird Dashboard (includes free test credits)
MessageBird Live API Key: Required for Verify API (test keys don't support verification). Create at Dashboard → Developers → API access (REST)
Phone Number Format: E.164 international format required (e.g., +14155552671 for US, +447700900000 for UK)
Basic Knowledge: JavaScript, async/await, RESTful APIs, React or Vue basics
(Optional) Testing Tools: Postman or curl for backend API testing
Expected Outcome:
Backend server with three OTP endpoints handling generation, verification, and status checks
Frontend application with phone input form and OTP code entry interface
Complete security implementation with rate limiting, attempt tracking, and expiration handling
Production-ready error handling for common failures (invalid numbers, expired codes, max attempts exceeded)
1. Setting up the Backend Project
Initialize the Node.js backend with MessageBird SDK and security dependencies.
Backend Setup Steps
Create Backend Directory:
Bash
mkdir messagebird-otp-backend
cd messagebird-otp-backend
npm init -y
Create a service module to interact with MessageBird's Verify API for generating and verifying OTP codes.
Edit src/messagebirdService.js:
JavaScript
// src/messagebirdService.jsimport messagebird from'messagebird';
import dotenv from'dotenv';
dotenv.config();
// Validate API key presenceif (!process.env.MESSAGEBIRD_API_KEY) {
console.error('Error: MESSAGEBIRD_API_KEY is not set in .env file');
process.exit(1);
}
// Initialize MessageBird clientconst client = messagebird(process.env.MESSAGEBIRD_API_KEY);
// Configuration from environment variables with defaultsconstOTP_CONFIG = {
timeout: parseInt(process.env.OTP_TIMEOUT) || 300, // Default: 5 minutestokenLength: parseInt(process.env.OTP_TOKEN_LENGTH) || 6, // Default: 6 digitsmaxAttempts: parseInt(process.env.OTP_MAX_ATTEMPTS) || 3// Default: 3 attempts
};
/**
* Generate and send OTP code to phone number using MessageBird Verify API
* @param {string} phoneNumber - E.164 format phone number (e.g., +14155552671)
* @returns {Promise<object>} - Verify object with id, status, validUntil
* @throws {Error} - If verification creation fails
*/exportconstgenerateOTP = async (phoneNumber) => {
returnnewPromise((resolve, reject) => {
// Create verification request with MessageBird
client.verify.create(phoneNumber, {
originator: 'YourApp', // Sender ID (max 11 chars, alphanumeric not supported in US)template: 'Your verification code is %token.', // %token replaced with generated codetype: 'sms', // Options: sms, flash, tts (voice), emailtimeout: OTP_CONFIG.timeout, // Expiration time in seconds (30-172800)tokenLength: OTP_CONFIG.tokenLength, // Code length (6-10 digits)datacoding: 'plain'// Options: plain (GSM), unicode, auto
}, (error, response) => {
if (error) {
console.error('MessageBird Verify creation error:', error);
// Common errors: invalid number format, insufficient balance, API key issuesreturnreject(newError(error.errors?.[0]?.description || 'Failed to generate OTP'));
}
console.log('OTP generated successfully:', {
id: response.id,
recipient: response.recipient,
status: response.status,
validUntil: response.validUntilDatetime
});
resolve({
verifyId: response.id,
status: response.status,
validUntil: response.validUntilDatetime,
recipient: response.recipient
});
});
});
};
/**
* Verify OTP code entered by user
* @param {string} verifyId - The verification ID from generateOTP
* @param {string} token - The code entered by the user
* @returns {Promise<object>} - Verification result with status
* @throws {Error} - If verification fails (wrong code, expired, max attempts)
*/exportconstverifyOTP = async (verifyId, token) => {
returnnewPromise((resolve, reject) => {
client.verify.verify(verifyId, token, (error, response) => {
if (error) {
console.error('MessageBird verification error:', error);
// Common errors: token mismatch, expired verification, too many attemptsconst errorMsg = error.errors?.[0]?.description || 'Verification failed';
returnreject(newError(errorMsg));
}
console.log('OTP verified successfully:', {
id: response.id,
status: response.status
});
resolve({
verifyId: response.id,
status: response.status, // Will be "verified" on successrecipient: response.recipient
});
});
});
};
/**
* Get current status of a verification (for checking expiry, attempts)
* @param {string} verifyId - The verification ID
* @returns {Promise<object>} - Verification object with current status
*/exportconstgetVerifyStatus = async (verifyId) => {
returnnewPromise((resolve, reject) => {
client.verify.view(verifyId, (error, response) => {
if (error) {
console.error('MessageBird verify status error:', error);
returnreject(newError(error.errors?.[0]?.description || 'Failed to get status'));
}
resolve({
verifyId: response.id,
status: response.status, // sent, verified, expired, failedcreatedAt: response.createdDatetime,
validUntil: response.validUntilDatetime,
recipient: response.recipient
});
});
});
};
/**
* Delete/cancel a verification (useful for cleanup or resend flows)
* @param {string} verifyId - The verification ID to delete
*/exportconstdeleteVerification = async (verifyId) => {
returnnewPromise((resolve, reject) => {
client.verify.delete(verifyId, (error) => {
if (error) {
console.error('MessageBird verification deletion error:', error);
returnreject(newError('Failed to delete verification'));
}
resolve({ deleted: true });
});
});
};
Key Implementation Notes:
Timeout Range: MessageBird supports 30–172,800 seconds (30s to 2 days). For security, 5–10 minutes is recommended for most applications (NIST SP 800-63B suggests short-lived OTP tokens).
Token Length: 6 digits provides 1 million combinations. With maxAttempts=3, brute-force probability is 0.0003% per verification session (3/1,000,000).
Originator Restrictions: Alphanumeric sender IDs (e.g., "YourApp") are not supported in the United States and require pre-registration in many countries. Use a MessageBird virtual number for reliable delivery (MessageBird sender ID guide).
3. Building the API Endpoints
Create Express server with three OTP endpoints: generate, verify, and status.
Edit src/server.js:
JavaScript
// src/server.jsimport express from'express';
import cors from'cors';
import rateLimit from'express-rate-limit';
import dotenv from'dotenv';
import { generateOTP, verifyOTP, getVerifyStatus } from'./messagebirdService.js';
dotenv.config();
const app = express();
constPORT = process.env.PORT || 3001;
// Middleware
app.use(express.json());
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? 'https://your-frontend-domain.com'
: 'http://localhost:5173'// Vite default port
}));
// Rate limiting for OTP generation (prevent SMS bombing)const otpGenerateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 5, // Max 5 OTP requests per phone per windowmessage: { success: false, message: 'Too many OTP requests. Try again later.' },
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.body.phoneNumber || req.ip// Rate limit by phone number
});
// Rate limiting for OTP verification (prevent brute force)const otpVerifyLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 10, // Max 10 verification attempts per windowmessage: { success: false, message: 'Too many verification attempts. Try again later.' },
standardHeaders: true,
legacyHeaders: false
});
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: newDate().toISOString(),
service: 'MessageBird OTP API'
});
});
// --- OTP Generation Endpoint ---
app.post('/api/otp/generate', otpGenerateLimiter, async (req, res) => {
const { phoneNumber } = req.body;
// Validate phone number presenceif (!phoneNumber) {
return res.status(400).json({
success: false,
message: 'Phone number is required'
});
}
// Validate E.164 format (starts with +, followed by 10-15 digits)const e164Regex = /^\+[1-9]\d{10,14}$/;
if (!e164Regex.test(phoneNumber)) {
return res.status(400).json({
success: false,
message: 'Invalid phone number format. Use E.164 format (e.g., +14155552671)'
});
}
try {
const result = awaitgenerateOTP(phoneNumber);
res.status(200).json({
success: true,
message: 'OTP sent successfully',
data: {
verifyId: result.verifyId,
validUntil: result.validUntil,
status: result.status
}
});
} catch (error) {
console.error('OTP generation error:', error.message);
res.status(500).json({
success: false,
message: 'Failed to send OTP',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
// --- OTP Verification Endpoint ---
app.post('/api/otp/verify', otpVerifyLimiter, async (req, res) => {
const { verifyId, token } = req.body;
// Validate inputsif (!verifyId || !token) {
return res.status(400).json({
success: false,
message: 'Verification ID and token are required'
});
}
// Validate token format (should be numeric, length matches config)const tokenLength = parseInt(process.env.OTP_TOKEN_LENGTH) || 6;
if (!/^\d+$/.test(token) || token.length !== tokenLength) {
return res.status(400).json({
success: false,
message: `Token must be ${tokenLength} digits`
});
}
try {
const result = awaitverifyOTP(verifyId, token);
res.status(200).json({
success: true,
message: 'Phone number verified successfully',
data: {
verifyId: result.verifyId,
status: result.status,
recipient: result.recipient
}
});
} catch (error) {
console.error('OTP verification error:', error.message);
// Provide specific error messages for common failureslet statusCode = 400;
let message = 'Verification failed';
if (error.message.includes('token does not match') || error.message.includes('invalid')) {
message = 'Invalid verification code';
} elseif (error.message.includes('expired')) {
message = 'Verification code has expired';
statusCode = 410; // 410 Gone
} elseif (error.message.includes('attempts')) {
message = 'Maximum verification attempts exceeded';
statusCode = 429; // 429 Too Many Requests
}
res.status(statusCode).json({
success: false,
message,
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
// --- OTP Status Check Endpoint ---
app.get('/api/otp/status/:verifyId', async (req, res) => {
const { verifyId } = req.params;
if (!verifyId) {
return res.status(400).json({
success: false,
message: 'Verification ID is required'
});
}
try {
const result = awaitgetVerifyStatus(verifyId);
res.status(200).json({
success: true,
data: result
});
} catch (error) {
console.error('Status check error:', error.message);
res.status(404).json({
success: false,
message: 'Verification not found or expired'
});
}
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({
success: false,
message: 'Internal server error'
});
});
// Start server
app.listen(PORT, () => {
console.log(`✅ MessageBird OTP API running on http://localhost:${PORT}`);
console.log(`📱 Endpoints available:`);
console.log(` POST http://localhost:${PORT}/api/otp/generate`);
console.log(` POST http://localhost:${PORT}/api/otp/verify`);
console.log(` GET http://localhost:${PORT}/api/otp/status/:verifyId`);
});
Security Implementation Details:
Phone-based Rate Limiting: The generate endpoint limits requests by phone number (5 per 15 min) to prevent SMS bombing attacks where attackers flood a number with verification codes.
Attempt Limiting: MessageBird tracks verification attempts server-side (configurable 1–10 via maxAttempts). Combined with API rate limiting (10 verify requests per 15 min), this provides defense-in-depth against brute-force attacks.
Specific Error Responses: Different HTTP status codes help clients handle failures appropriately (400 for invalid input, 410 for expired codes, 429 for rate limits).
4. Frontend Setup with Vite
Create a modern frontend using Vite with either React or Vue for the OTP interface.
Choose Your Framework:
Option A: React with Vite
Bash
cd ..
npm create vite@latest messagebird-otp-frontend -- --template react
cd messagebird-otp-frontend
npm install
npm install axios
Option B: Vue 3 with Vite
Bash
cd ..
npm create vite@latest messagebird-otp-frontend -- --template vue
cd messagebird-otp-frontend
npm install
npm install axios
{"success":true,"message":"OTP sent successfully","data":{"verifyId":"4e213b01155d1e35a9d9571v00162985","validUntil":"2024-01-15T14:35:00Z","status":"sent"}}
For production applications, track verification attempts in a database:
PostgreSQL Schema Example:
SQL
CREATE TABLE otp_verifications (
id SERIAL PRIMARY KEY,
phone_number VARCHAR(20) NOT NULL,
verify_id VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL, -- sent, verified, expired, failed
attempts INTDEFAULT0,
created_at TIMESTAMPDEFAULT NOW(),
verified_at TIMESTAMP,
expires_at TIMESTAMPNOT NULL,
ip_address INET,
user_agent TEXT
);
CREATE INDEX idx_phone_created ON otp_verifications(phone_number, created_at);
CREATE INDEX idx_verify_id ON otp_verifications(verify_id);
Benefits:
Audit trail for compliance (GDPR Article 30)
Security analytics (detect abuse patterns)
User support (verify code was sent to correct number)
Resend OTP Functionality
Add cooldown period to prevent abuse:
JavaScript
// Backend: Track last send time per phone numberconst sendCooldown = newMap(); // Use Redis in production
app.post('/api/otp/resend', otpGenerateLimiter, async (req, res) => {
const { phoneNumber } = req.body;
const lastSend = sendCooldown.get(phoneNumber);
if (lastSend && Date.now() - lastSend < 60000) {
return res.status(429).json({
success: false,
message: 'Wait 60 seconds before requesting another code'
});
}
// Generate new OTP (same logic as /generate)const result = awaitgenerateOTP(phoneNumber);
sendCooldown.set(phoneNumber, Date.now());
res.status(200).json({ success: true, data: result });
});
Multi-Language Support
MessageBird Verify API supports 35+ languages for voice OTP (API documentation):
JavaScript
client.verify.create(phoneNumber, {
type: 'tts', // Text-to-speech voice calllanguage: 'es-mx', // Spanish (Mexico)voice: 'female', // male or female// ... other options
});
Supported languages include: English (en-us, en-gb), Spanish (es-es, es-mx), French (fr-fr), German (de-de), Mandarin (zh-cn), Japanese (ja-jp), and many more.
Frequently Asked Questions
How does MessageBird Verify API generate OTP codes?
MessageBird Verify API automatically generates cryptographically secure random numeric codes (6–10 digits configurable) server-side when you create a verification object. The API handles token generation, storage, expiration, and delivery—you never see or manage the actual code in your backend, which eliminates implementation vulnerabilities (MessageBird Verify API docs).
What's the difference between MessageBird SMS API and Verify API?
The SMS API (client.messages.create()) sends arbitrary text messages and requires you to manually generate OTP codes, track expiration, and manage verification attempts. The Verify API (client.verify.create()) is purpose-built for OTP/2FA, automatically handling token generation, validation, expiration, and attempt limiting. Use Verify API for authentication flows and SMS API for notifications or marketing messages.
How long should OTP codes be valid?
According to NIST SP 800-63B guidelines (2023), OTP tokens should expire quickly to minimize attack windows. Recommended ranges: 5–10 minutes for standard authentication (balances security and UX), 30–60 seconds for high-security transactions (banking, medical records), 15–30 minutes for account recovery flows (users may need to check email for password reset). MessageBird Verify API supports 30 seconds to 2 days.
Can I use MessageBird Verify API with free trial accounts?
Yes, MessageBird provides free trial credits for testing, but you must use a live API key (not test mode key) for Verify API functionality. Trial accounts may have destination restrictions—verify that you can send to your test phone numbers in the MessageBird Dashboard. As of 2024, most trial accounts include €10–20 in free credits sufficient for 100–200 SMS verifications depending on destination country.
How do I prevent SMS bombing attacks on my OTP endpoint?
Implement multi-layer rate limiting: (1) Phone-number-based limits (3–5 OTP requests per number per hour) using express-rate-limit with custom keyGenerator, (2) IP-based limits (10 requests per IP per hour) as backup, (3) Consider CAPTCHA after 2 failed generation attempts, (4) Monitor for unusual patterns (alerts when >50 requests/hour from single source). MessageBird also applies account-level rate limits for additional protection.
What happens if a user enters the wrong OTP code multiple times?
MessageBird Verify API tracks verification attempts (configurable 1–10 via maxAttempts, default 3). After exceeding max attempts, the verification status changes to "failed" and the API returns an error on subsequent verify requests. The verification ID becomes invalid and the user must request a new OTP code. This prevents brute-force attacks (3 attempts = 0.0003% success probability with 6-digit codes).
Is SMS-based OTP secure enough for sensitive applications?
SMS OTP provides moderate security suitable for most applications (account login, e-commerce checkouts, identity verification). However, NIST SP 800-63B deprecates SMS for high-security contexts due to vulnerabilities (SIM swapping, SS7 interception, phishing). For banking, healthcare, or government applications, consider: (1) App-based TOTP (Google Authenticator, Authy), (2) Hardware tokens (YubiKey), (3) MessageBird's WhatsApp verification (more secure than SMS), or (4) Multi-factor authentication combining SMS + another factor.
How do I handle users in countries with poor SMS delivery?
MessageBird Verify API supports alternative channels: (1) Voice OTP (TTS): Call user and speak code aloud (set type: 'tts'), useful for areas with SMS blocking, (2) WhatsApp: More reliable delivery in developing markets (requires WhatsApp Business API integration), (3) Email: For non-mobile users (set type: 'email'). Monitor delivery rates by country in MessageBird Dashboard and automatically fallback to voice for countries with <95% SMS delivery success.
Can I customize the OTP message template?
Yes, MessageBird Verify API allows custom templates via the template parameter. The string must include %token placeholder which MessageBird replaces with the generated code. Example templates: "Your verification code is %token." (default), "Use code %token to log in to YourApp", "%token is your one-time password (valid 5 minutes)". Keep messages under 160 characters (GSM-7) or 70 characters (Unicode) to avoid multi-part SMS charges.
How do I implement OTP for user registration vs. login?
Registration flow: (1) User enters phone number during signup, (2) Send OTP via Verify API, (3) Verify code, (4) Create account with verified phone number in database. Login flow: (1) User enters username/password, (2) If credentials valid, send OTP to registered phone, (3) Verify code, (4) Issue session token/JWT. For enhanced security, require OTP on every login (2FA) or only for high-risk events (new device, unusual location, password change).
Related Resources
For comprehensive guides on SMS authentication and security:
SMS vs. TOTP for 2FA: Security Comparison: Analysis of SMS OTP vulnerabilities and alternatives (check security blogs like Krebs on Security)
React Hook Form for OTP Inputs: Advanced form validation patterns for production applications
Vue 3 Composition API Best Practices: Modern Vue patterns for component architecture
Frequently asked questions
Use the Vonage Messages API with the Node.js SDK and Express. Create an API endpoint that accepts recipient details and message content, then uses the SDK to send the SMS via Vonage.
It's a cloud-based API by Vonage that allows developers to send and receive SMS messages programmatically. This guide focuses on sending SMS using the Messages API via their Node.js Server SDK.
Express simplifies creating API endpoints to handle SMS requests, manage middleware, and handle HTTP requests/responses. This guide uses Express to build a /api/send-sms endpoint.
Use the private key path for local development, as specified in your .env file with VONAGE_PRIVATE_KEY_PATH. Use the content variable, VONAGE_PRIVATE_KEY_CONTENT, in production environments for security, and populating the variable through a deployment pipeline, avoiding the need to store the key file directly on the server.
Yes, you can use an alphanumeric sender ID (e.g., 'MyBrand') instead of a phone number. Note that support varies by country, and replies may not be possible. Use the from parameter in your API request as outlined in section 8. For this project, we'll default to your VONAGE_NUMBER.
Log in to the Vonage Dashboard, create a new application, generate your keys (saving the private key securely), and link your Vonage number to the application. Then, set your credentials in your project's .env file. Be sure the default SMS API is set to "Messages API".
Nodemon automatically restarts the Node.js server during development when code changes are detected, streamlining the development process. It's installed as a development dependency.
Implement try...catch blocks to handle errors during API calls. The Vonage SDK might provide specific error codes/messages you can use for more detailed error handling. For production, map these to user-friendly responses.
A health check endpoint (e.g., /health) allows monitoring systems to quickly check the status of your application. It typically returns a simple response like { status: 'OK' } if the server is running.
Separating concerns makes your code more organized, testable, and maintainable. It isolates the Vonage API interaction, simplifying your main server file (server.js) and allowing independent testing of the Vonage service logic.
Use API keys, JWTs, or other authentication/authorization methods to protect your API endpoint. Also, implement input validation, rate limiting (e.g., with express-rate-limit), and use security headers (e.g., with helmet).
DLRs (Delivery Receipts) provide delivery status updates (delivered, failed, etc.). Configure a 'Status URL' in your Vonage application settings. Implement an endpoint to receive POST requests from Vonage containing DLR information, using the message_uuid to correlate responses.
Vonage largely handles encoding automatically, switching between GSM-7 (160 chars/segment) and UCS-2 (70 chars/segment) as needed. Be aware of these limits, especially for non-GSM characters (emojis, accented letters), as messages are split into segments and billing is per segment.
Never include the private key file (.env or private.key) in version control. In production, store the content of private.key in a secure environment variable like VONAGE_PRIVATE_KEY_CONTENT using your platform's secrets management features (e.g., Heroku Config Vars).
Build a production-ready SMS-based One-Time Password (OTP) two-factor authentication (2FA) system using MessageBird's Verify API with a Node.js/Express backend and modern Vite-powered frontend (React or Vue). Implement secure code generation, verification flows, and handle edge cases for real-world applications.
What is OTP/2FA and Why MessageBird?
One-Time Password (OTP) authentication sends a temporary numeric code (typically 4–10 digits) to a user's mobile phone via SMS. Users must enter this code within a limited timeframe (usually 30 seconds to 2 days) to verify their identity. Two-Factor Authentication (2FA) adds this second verification layer beyond username/password, significantly reducing unauthorized access even if passwords are compromised (NIST SP 800-63B recommends 2FA for sensitive applications as of 2023).
MessageBird's Verify API simplifies OTP implementation by:
According to MessageBird's official Verify API documentation, the service handles token generation server-side, eliminating client-side security risks and simplifying implementation compared to manual OTP solutions.
Project Overview and Goals
Goal: Build a full-stack OTP/2FA system with:
/api/otp/generate), verifying codes (/api/otp/verify), and viewing verification status (/api/otp/status)Problem Solved: Adds robust two-factor authentication to protect user accounts, secure transactions, verify phone numbers during registration, and meet compliance requirements for financial services, healthcare, or e-commerce applications.
Technologies Used:
messagebirdnpm package: Official MessageBird SDK for Node.js (v4.0.1 as of January 2022, Apache-2.0 license, GitHub repository)dotenv: Environment variable managementaxios: HTTP client for API requests from frontendexpress-rate-limit: Protection against brute-force attacksSystem Architecture:
The complete OTP flow involves both frontend and backend coordination:
Prerequisites:
Expected Outcome:
1. Setting up the Backend Project
Initialize the Node.js backend with MessageBird SDK and security dependencies.
Backend Setup Steps
Create Backend Directory:
mkdir messagebird-otp-backend cd messagebird-otp-backend npm init -yEnable ES Modules: Edit
package.jsonto add:{ "type": "module", "scripts": { "dev": "nodemon src/server.js", "start": "node src/server.js" } }Install Backend Dependencies:
messagebird: Official SDK (v4.0.1, npm package)express-rate-limit: Prevent brute-force OTP guessing attackscors: Enable frontend-backend communication during developmentCreate Project Structure:
mkdir src touch src/server.js src/messagebirdService.js .env .gitignoreStructure:
Configure
.gitignore:Configure
.env:Important: MessageBird Verify API requires a live API key (not test mode). Get yours at MessageBird Dashboard → Developers → API access. According to MessageBird's Verify API documentation, test keys do not support verification functionality (effective as of 2024).
2. Implementing MessageBird Verify Service
Create a service module to interact with MessageBird's Verify API for generating and verifying OTP codes.
Edit
src/messagebirdService.js:// src/messagebirdService.js import messagebird from 'messagebird'; import dotenv from 'dotenv'; dotenv.config(); // Validate API key presence if (!process.env.MESSAGEBIRD_API_KEY) { console.error('Error: MESSAGEBIRD_API_KEY is not set in .env file'); process.exit(1); } // Initialize MessageBird client const client = messagebird(process.env.MESSAGEBIRD_API_KEY); // Configuration from environment variables with defaults const OTP_CONFIG = { timeout: parseInt(process.env.OTP_TIMEOUT) || 300, // Default: 5 minutes tokenLength: parseInt(process.env.OTP_TOKEN_LENGTH) || 6, // Default: 6 digits maxAttempts: parseInt(process.env.OTP_MAX_ATTEMPTS) || 3 // Default: 3 attempts }; /** * Generate and send OTP code to phone number using MessageBird Verify API * @param {string} phoneNumber - E.164 format phone number (e.g., +14155552671) * @returns {Promise<object>} - Verify object with id, status, validUntil * @throws {Error} - If verification creation fails */ export const generateOTP = async (phoneNumber) => { return new Promise((resolve, reject) => { // Create verification request with MessageBird client.verify.create(phoneNumber, { originator: 'YourApp', // Sender ID (max 11 chars, alphanumeric not supported in US) template: 'Your verification code is %token.', // %token replaced with generated code type: 'sms', // Options: sms, flash, tts (voice), email timeout: OTP_CONFIG.timeout, // Expiration time in seconds (30-172800) tokenLength: OTP_CONFIG.tokenLength, // Code length (6-10 digits) datacoding: 'plain' // Options: plain (GSM), unicode, auto }, (error, response) => { if (error) { console.error('MessageBird Verify creation error:', error); // Common errors: invalid number format, insufficient balance, API key issues return reject(new Error(error.errors?.[0]?.description || 'Failed to generate OTP')); } console.log('OTP generated successfully:', { id: response.id, recipient: response.recipient, status: response.status, validUntil: response.validUntilDatetime }); resolve({ verifyId: response.id, status: response.status, validUntil: response.validUntilDatetime, recipient: response.recipient }); }); }); }; /** * Verify OTP code entered by user * @param {string} verifyId - The verification ID from generateOTP * @param {string} token - The code entered by the user * @returns {Promise<object>} - Verification result with status * @throws {Error} - If verification fails (wrong code, expired, max attempts) */ export const verifyOTP = async (verifyId, token) => { return new Promise((resolve, reject) => { client.verify.verify(verifyId, token, (error, response) => { if (error) { console.error('MessageBird verification error:', error); // Common errors: token mismatch, expired verification, too many attempts const errorMsg = error.errors?.[0]?.description || 'Verification failed'; return reject(new Error(errorMsg)); } console.log('OTP verified successfully:', { id: response.id, status: response.status }); resolve({ verifyId: response.id, status: response.status, // Will be "verified" on success recipient: response.recipient }); }); }); }; /** * Get current status of a verification (for checking expiry, attempts) * @param {string} verifyId - The verification ID * @returns {Promise<object>} - Verification object with current status */ export const getVerifyStatus = async (verifyId) => { return new Promise((resolve, reject) => { client.verify.view(verifyId, (error, response) => { if (error) { console.error('MessageBird verify status error:', error); return reject(new Error(error.errors?.[0]?.description || 'Failed to get status')); } resolve({ verifyId: response.id, status: response.status, // sent, verified, expired, failed createdAt: response.createdDatetime, validUntil: response.validUntilDatetime, recipient: response.recipient }); }); }); }; /** * Delete/cancel a verification (useful for cleanup or resend flows) * @param {string} verifyId - The verification ID to delete */ export const deleteVerification = async (verifyId) => { return new Promise((resolve, reject) => { client.verify.delete(verifyId, (error) => { if (error) { console.error('MessageBird verification deletion error:', error); return reject(new Error('Failed to delete verification')); } resolve({ deleted: true }); }); }); };Key Implementation Notes:
Timeout Range: MessageBird supports 30–172,800 seconds (30s to 2 days). For security, 5–10 minutes is recommended for most applications (NIST SP 800-63B suggests short-lived OTP tokens).
Token Length: 6 digits provides 1 million combinations. With
maxAttempts=3, brute-force probability is 0.0003% per verification session (3/1,000,000).Originator Restrictions: Alphanumeric sender IDs (e.g., "YourApp") are not supported in the United States and require pre-registration in many countries. Use a MessageBird virtual number for reliable delivery (MessageBird sender ID guide).
3. Building the API Endpoints
Create Express server with three OTP endpoints: generate, verify, and status.
Edit
src/server.js:// src/server.js import express from 'express'; import cors from 'cors'; import rateLimit from 'express-rate-limit'; import dotenv from 'dotenv'; import { generateOTP, verifyOTP, getVerifyStatus } from './messagebirdService.js'; dotenv.config(); const app = express(); const PORT = process.env.PORT || 3001; // Middleware app.use(express.json()); app.use(cors({ origin: process.env.NODE_ENV === 'production' ? 'https://your-frontend-domain.com' : 'http://localhost:5173' // Vite default port })); // Rate limiting for OTP generation (prevent SMS bombing) const otpGenerateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Max 5 OTP requests per phone per window message: { success: false, message: 'Too many OTP requests. Try again later.' }, standardHeaders: true, legacyHeaders: false, keyGenerator: (req) => req.body.phoneNumber || req.ip // Rate limit by phone number }); // Rate limiting for OTP verification (prevent brute force) const otpVerifyLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // Max 10 verification attempts per window message: { success: false, message: 'Too many verification attempts. Try again later.' }, standardHeaders: true, legacyHeaders: false }); // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'OK', timestamp: new Date().toISOString(), service: 'MessageBird OTP API' }); }); // --- OTP Generation Endpoint --- app.post('/api/otp/generate', otpGenerateLimiter, async (req, res) => { const { phoneNumber } = req.body; // Validate phone number presence if (!phoneNumber) { return res.status(400).json({ success: false, message: 'Phone number is required' }); } // Validate E.164 format (starts with +, followed by 10-15 digits) const e164Regex = /^\+[1-9]\d{10,14}$/; if (!e164Regex.test(phoneNumber)) { return res.status(400).json({ success: false, message: 'Invalid phone number format. Use E.164 format (e.g., +14155552671)' }); } try { const result = await generateOTP(phoneNumber); res.status(200).json({ success: true, message: 'OTP sent successfully', data: { verifyId: result.verifyId, validUntil: result.validUntil, status: result.status } }); } catch (error) { console.error('OTP generation error:', error.message); res.status(500).json({ success: false, message: 'Failed to send OTP', error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }); // --- OTP Verification Endpoint --- app.post('/api/otp/verify', otpVerifyLimiter, async (req, res) => { const { verifyId, token } = req.body; // Validate inputs if (!verifyId || !token) { return res.status(400).json({ success: false, message: 'Verification ID and token are required' }); } // Validate token format (should be numeric, length matches config) const tokenLength = parseInt(process.env.OTP_TOKEN_LENGTH) || 6; if (!/^\d+$/.test(token) || token.length !== tokenLength) { return res.status(400).json({ success: false, message: `Token must be ${tokenLength} digits` }); } try { const result = await verifyOTP(verifyId, token); res.status(200).json({ success: true, message: 'Phone number verified successfully', data: { verifyId: result.verifyId, status: result.status, recipient: result.recipient } }); } catch (error) { console.error('OTP verification error:', error.message); // Provide specific error messages for common failures let statusCode = 400; let message = 'Verification failed'; if (error.message.includes('token does not match') || error.message.includes('invalid')) { message = 'Invalid verification code'; } else if (error.message.includes('expired')) { message = 'Verification code has expired'; statusCode = 410; // 410 Gone } else if (error.message.includes('attempts')) { message = 'Maximum verification attempts exceeded'; statusCode = 429; // 429 Too Many Requests } res.status(statusCode).json({ success: false, message, error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }); // --- OTP Status Check Endpoint --- app.get('/api/otp/status/:verifyId', async (req, res) => { const { verifyId } = req.params; if (!verifyId) { return res.status(400).json({ success: false, message: 'Verification ID is required' }); } try { const result = await getVerifyStatus(verifyId); res.status(200).json({ success: true, data: result }); } catch (error) { console.error('Status check error:', error.message); res.status(404).json({ success: false, message: 'Verification not found or expired' }); } }); // Error handling middleware app.use((err, req, res, next) => { console.error('Unhandled error:', err); res.status(500).json({ success: false, message: 'Internal server error' }); }); // Start server app.listen(PORT, () => { console.log(`✅ MessageBird OTP API running on http://localhost:${PORT}`); console.log(`📱 Endpoints available:`); console.log(` POST http://localhost:${PORT}/api/otp/generate`); console.log(` POST http://localhost:${PORT}/api/otp/verify`); console.log(` GET http://localhost:${PORT}/api/otp/status/:verifyId`); });Security Implementation Details:
Phone-based Rate Limiting: The generate endpoint limits requests by phone number (5 per 15 min) to prevent SMS bombing attacks where attackers flood a number with verification codes.
Attempt Limiting: MessageBird tracks verification attempts server-side (configurable 1–10 via
maxAttempts). Combined with API rate limiting (10 verify requests per 15 min), this provides defense-in-depth against brute-force attacks.Specific Error Responses: Different HTTP status codes help clients handle failures appropriately (400 for invalid input, 410 for expired codes, 429 for rate limits).
4. Frontend Setup with Vite
Create a modern frontend using Vite with either React or Vue for the OTP interface.
Choose Your Framework:
Option A: React with Vite
cd .. npm create vite@latest messagebird-otp-frontend -- --template react cd messagebird-otp-frontend npm install npm install axiosOption B: Vue 3 with Vite
cd .. npm create vite@latest messagebird-otp-frontend -- --template vue cd messagebird-otp-frontend npm install npm install axiosFrontend Project Structure
Configure API Endpoint
Create
.envin frontend root:Note: Vite requires
VITE_prefix for environment variables to be exposed to client code (Vite env documentation).5. Building the Frontend Components
API Service Layer
Create
src/services/api.js:// src/services/api.js import axios from 'axios'; const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'; const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json' }, timeout: 10000 // 10 second timeout }); export const otpAPI = { /** * Request OTP code for phone number * @param {string} phoneNumber - E.164 format * @returns {Promise<{verifyId, validUntil, status}>} */ generateOTP: async (phoneNumber) => { const response = await apiClient.post('/api/otp/generate', { phoneNumber }); return response.data; }, /** * Verify OTP code entered by user * @param {string} verifyId - Verification ID from generateOTP * @param {string} token - User-entered code * @returns {Promise<{status, recipient}>} */ verifyOTP: async (verifyId, token) => { const response = await apiClient.post('/api/otp/verify', { verifyId, token }); return response.data; }, /** * Check verification status * @param {string} verifyId * @returns {Promise<{status, validUntil, createdAt}>} */ getStatus: async (verifyId) => { const response = await apiClient.get(`/api/otp/status/${verifyId}`); return response.data; } };React Implementation
React: PhoneInput Component
Create
src/components/PhoneInput.jsx:// src/components/PhoneInput.jsx import { useState } from 'react'; export default function PhoneInput({ onSubmit, loading }) { const [phoneNumber, setPhoneNumber] = useState(''); const [error, setError] = useState(''); const validatePhone = (phone) => { // E.164 format validation const e164Regex = /^\+[1-9]\d{10,14}$/; return e164Regex.test(phone); }; const handleSubmit = (e) => { e.preventDefault(); setError(''); if (!phoneNumber) { setError('Phone number is required'); return; } if (!validatePhone(phoneNumber)) { setError('Invalid format. Use E.164 (e.g., +14155552671)'); return; } onSubmit(phoneNumber); }; return ( <div className="phone-input-container"> <h2>Enter Your Phone Number</h2> <p>We'll send you a verification code via SMS</p> <form onSubmit={handleSubmit}> <input type="tel" value={phoneNumber} onChange={(e) => setPhoneNumber(e.target.value)} placeholder="+1 415 555 2671" disabled={loading} autoFocus className="phone-input" /> {error && <p className="error-message">{error}</p>} <button type="submit" disabled={loading} className="submit-button"> {loading ? 'Sending…' : 'Send Code'} </button> </form> <p className="help-text"> Format: +[country code][number] (e.g., +14155552671 for US) </p> </div> ); }React: OTPInput Component
Create
src/components/OTPInput.jsx:// src/components/OTPInput.jsx import { useState, useRef, useEffect } from 'react'; export default function OTPInput({ length = 6, onComplete, onResend, loading, expiryTime }) { const [otp, setOtp] = useState(new Array(length).fill('')); const [timeLeft, setTimeLeft] = useState(null); const inputRefs = useRef([]); // Calculate time remaining useEffect(() => { if (!expiryTime) return; const interval = setInterval(() => { const now = new Date().getTime(); const expiry = new Date(expiryTime).getTime(); const remaining = Math.max(0, Math.floor((expiry - now) / 1000)); setTimeLeft(remaining); if (remaining === 0) { clearInterval(interval); } }, 1000); return () => clearInterval(interval); }, [expiryTime]); const handleChange = (index, value) => { // Only allow digits if (!/^\d*$/.test(value)) return; const newOtp = [...otp]; newOtp[index] = value.slice(-1); // Only last character setOtp(newOtp); // Auto-focus next input if (value && index < length - 1) { inputRefs.current[index + 1]?.focus(); } // Auto-submit when complete if (newOtp.every(digit => digit !== '') && !loading) { onComplete(newOtp.join('')); } }; const handleKeyDown = (index, e) => { // Handle backspace to move to previous input if (e.key === 'Backspace' && !otp[index] && index > 0) { inputRefs.current[index - 1]?.focus(); } }; const handlePaste = (e) => { e.preventDefault(); const pastedData = e.clipboardData.getData('text').slice(0, length); if (!/^\d+$/.test(pastedData)) return; const newOtp = pastedData.split('').concat(new Array(length).fill('')).slice(0, length); setOtp(newOtp); // Focus last filled input or next empty const nextIndex = Math.min(pastedData.length, length - 1); inputRefs.current[nextIndex]?.focus(); if (newOtp.every(digit => digit !== '')) { onComplete(newOtp.join('')); } }; const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; }; return ( <div className="otp-input-container"> <h2>Enter Verification Code</h2> <p>We sent a {length}-digit code to your phone</p> <div className="otp-inputs"> {otp.map((digit, index) => ( <input key={index} ref={(el) => (inputRefs.current[index] = el)} type="text" inputMode="numeric" maxLength={1} value={digit} onChange={(e) => handleChange(index, e.target.value)} onKeyDown={(e) => handleKeyDown(index, e)} onPaste={handlePaste} disabled={loading || timeLeft === 0} className="otp-digit" autoFocus={index === 0} /> ))} </div> {timeLeft !== null && ( <p className={`timer ${timeLeft < 30 ? 'warning' : ''}`}> {timeLeft > 0 ? `Code expires in ${formatTime(timeLeft)}` : 'Code has expired'} </p> )} <button onClick={onResend} disabled={loading || timeLeft > 0} className="resend-button" > {timeLeft > 0 ? 'Resend available after expiry' : 'Resend Code'} </button> </div> ); }React: Main App Component
Edit
src/App.jsx:// src/App.jsx import { useState } from 'react'; import PhoneInput from './components/PhoneInput'; import OTPInput from './components/OTPInput'; import { otpAPI } from './services/api'; import './App.css'; function App() { const [step, setStep] = useState('phone'); // 'phone', 'otp', 'success', 'error' const [phoneNumber, setPhoneNumber] = useState(''); const [verifyId, setVerifyId] = useState(''); const [validUntil, setValidUntil] = useState(''); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(''); const handlePhoneSubmit = async (phone) => { setLoading(true); setMessage(''); try { const response = await otpAPI.generateOTP(phone); if (response.success) { setPhoneNumber(phone); setVerifyId(response.data.verifyId); setValidUntil(response.data.validUntil); setStep('otp'); setMessage('Code sent! Check your phone.'); } } catch (error) { setMessage(error.response?.data?.message || 'Failed to send code. Try again.'); } finally { setLoading(false); } }; const handleOTPComplete = async (code) => { setLoading(true); setMessage(''); try { const response = await otpAPI.verifyOTP(verifyId, code); if (response.success) { setStep('success'); setMessage('✅ Phone number verified successfully!'); } } catch (error) { const errorMsg = error.response?.data?.message || 'Verification failed'; setMessage(`❌ ${errorMsg}`); // Show error state but stay on OTP screen for retry if (error.response?.status === 410 || error.response?.status === 429) { // Expired or max attempts – go back to phone input setTimeout(() => { setStep('phone'); setMessage(''); }, 3000); } } finally { setLoading(false); } }; const handleResend = () => { setStep('phone'); setMessage(''); // Optionally pre-fill phone number }; const handleReset = () => { setStep('phone'); setPhoneNumber(''); setVerifyId(''); setValidUntil(''); setMessage(''); }; return ( <div className="app-container"> <div className="card"> <h1>🔐 Phone Verification</h1> {message && ( <div className={`message ${step === 'success' ? 'success' : 'error'}`}> {message} </div> )} {step === 'phone' && ( <PhoneInput onSubmit={handlePhoneSubmit} loading={loading} /> )} {step === 'otp' && ( <OTPInput length={6} onComplete={handleOTPComplete} onResend={handleResend} loading={loading} expiryTime={validUntil} /> )} {step === 'success' && ( <div className="success-container"> <div className="success-icon">✓</div> <h2>Verification Successful!</h2> <p>Your phone number {phoneNumber} has been verified.</p> <button onClick={handleReset} className="button"> Verify Another Number </button> </div> )} </div> </div> ); } export default App;Vue 3 Implementation
Vue: PhoneInput Component
Create
src/components/PhoneInput.vue:Vue: OTPInput Component
Create
src/components/OTPInput.vue:Vue: Main App Component
Edit
src/App.vue:6. Adding Styles
Create
src/App.css(works for both React and Vue):/* src/App.css */ * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .app-container { width: 100%; max-width: 500px; } .card { background: white; border-radius: 16px; padding: 40px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); } h1 { text-align: center; color: #333; margin-bottom: 30px; font-size: 24px; } h2 { color: #444; margin-bottom: 10px; font-size: 20px; } p { color: #666; margin-bottom: 20px; line-height: 1.5; } .message { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-weight: 500; } .message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .phone-input-container, .otp-input-container { text-align: center; } .phone-input { width: 100%; padding: 14px 16px; font-size: 16px; border: 2px solid #ddd; border-radius: 8px; margin-bottom: 10px; transition: border-color 0.3s; } .phone-input:focus { outline: none; border-color: #667eea; } .phone-input:disabled { background-color: #f5f5f5; cursor: not-allowed; } .otp-inputs { display: flex; gap: 12px; justify-content: center; margin: 20px 0; } .otp-digit { width: 50px; height: 60px; font-size: 24px; text-align: center; border: 2px solid #ddd; border-radius: 8px; transition: all 0.3s; font-weight: 600; } .otp-digit:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } .otp-digit:disabled { background-color: #f5f5f5; cursor: not-allowed; } .timer { font-size: 14px; margin: 15px 0; font-weight: 500; } .timer.warning { color: #dc3545; } .submit-button, .resend-button, .button { width: 100%; padding: 14px; font-size: 16px; font-weight: 600; border: none; border-radius: 8px; cursor: pointer; transition: all 0.3s; margin-top: 10px; } .submit-button, .button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .submit-button:hover:not(:disabled), .button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4); } .submit-button:disabled, .resend-button:disabled { opacity: 0.6; cursor: not-allowed; } .resend-button { background: white; color: #667eea; border: 2px solid #667eea; } .resend-button:hover:not(:disabled) { background: #f8f9ff; } .error-message { color: #dc3545; font-size: 14px; margin-top: -10px; margin-bottom: 15px; } .help-text { font-size: 13px; color: #999; margin-top: 10px; } .success-container { text-align: center; padding: 20px 0; } .success-icon { width: 80px; height: 80px; background: #28a745; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 48px; margin: 0 auto 20px; } @media (max-width: 600px) { .card { padding: 30px 20px; } .otp-digit { width: 40px; height: 50px; font-size: 20px; } .otp-inputs { gap: 8px; } }7. Testing the Complete Application
Backend Testing
Start Backend Server:
cd messagebird-otp-backend npm run devTest with curl:
Generate OTP:
curl -X POST http://localhost:3001/api/otp/generate \ -H "Content-Type: application/json" \ -d '{"phoneNumber":"+14155552671"}'Response:
{ "success": true, "message": "OTP sent successfully", "data": { "verifyId": "4e213b01155d1e35a9d9571v00162985", "validUntil": "2024-01-15T14:35:00Z", "status": "sent" } }Verify OTP:
curl -X POST http://localhost:3001/api/otp/verify \ -H "Content-Type: application/json" \ -d '{"verifyId":"4e213b01155d1e35a9d9571v00162985","token":"123456"}'Frontend Testing
Start Frontend Dev Server:
cd messagebird-otp-frontend npm run devOpens at
http://localhost:5173Test Flow:
Common Testing Issues
MESSAGEBIRD_API_KEYis live key (not test) in backend.env+[country code][number]without spacesOTP_TIMEOUTin backend.envto 300 (5 minutes)VITE_API_URLin frontend.envmatches backend PORTserver.jsfor testing8. Security Best Practices
Production Security Checklist
Rate Limiting Configuration:
OTP Configuration:
HTTPS Requirements:
hstsmiddleware)API Key Protection:
Logging and Monitoring:
GDPR/Privacy Compliance:
Known Security Limitations
SMS-based OTP vulnerabilities (documented by NIST SP 800-63B):
Mitigation strategies:
9. Deployment Considerations
Backend Deployment (Node.js/Express)
Platform Options:
Environment Variables Setup:
# Production environment variables MESSAGEBIRD_API_KEY=your_live_key_here OTP_TIMEOUT=300 OTP_TOKEN_LENGTH=6 OTP_MAX_ATTEMPTS=3 PORT=3001 NODE_ENV=productionProcess Management: Use PM2 for production resilience:
npm install -g pm2 pm2 start src/server.js --name "messagebird-otp-api" pm2 save pm2 startupFrontend Deployment (Vite)
Platform Options:
Build and Deploy:
# Build production bundle npm run build # Output in dist/ directory # Deploy dist/ to hosting platformProduction Environment Variables: Create
.env.production:CORS Configuration for Production
Update backend
src/server.jsCORS settings:app.use(cors({ origin: process.env.NODE_ENV === 'production' ? ['https://your-frontend-domain.com', 'https://www.your-frontend-domain.com'] : 'http://localhost:5173', credentials: true }));10. Advanced Enhancements
Database Integration for Audit Trail
For production applications, track verification attempts in a database:
PostgreSQL Schema Example:
CREATE TABLE otp_verifications ( id SERIAL PRIMARY KEY, phone_number VARCHAR(20) NOT NULL, verify_id VARCHAR(100) NOT NULL, status VARCHAR(20) NOT NULL, -- sent, verified, expired, failed attempts INT DEFAULT 0, created_at TIMESTAMP DEFAULT NOW(), verified_at TIMESTAMP, expires_at TIMESTAMP NOT NULL, ip_address INET, user_agent TEXT ); CREATE INDEX idx_phone_created ON otp_verifications(phone_number, created_at); CREATE INDEX idx_verify_id ON otp_verifications(verify_id);Benefits:
Resend OTP Functionality
Add cooldown period to prevent abuse:
// Backend: Track last send time per phone number const sendCooldown = new Map(); // Use Redis in production app.post('/api/otp/resend', otpGenerateLimiter, async (req, res) => { const { phoneNumber } = req.body; const lastSend = sendCooldown.get(phoneNumber); if (lastSend && Date.now() - lastSend < 60000) { return res.status(429).json({ success: false, message: 'Wait 60 seconds before requesting another code' }); } // Generate new OTP (same logic as /generate) const result = await generateOTP(phoneNumber); sendCooldown.set(phoneNumber, Date.now()); res.status(200).json({ success: true, data: result }); });Multi-Language Support
MessageBird Verify API supports 35+ languages for voice OTP (API documentation):
client.verify.create(phoneNumber, { type: 'tts', // Text-to-speech voice call language: 'es-mx', // Spanish (Mexico) voice: 'female', // male or female // ... other options });Supported languages include: English (en-us, en-gb), Spanish (es-es, es-mx), French (fr-fr), German (de-de), Mandarin (zh-cn), Japanese (ja-jp), and many more.
Frequently Asked Questions
How does MessageBird Verify API generate OTP codes?
MessageBird Verify API automatically generates cryptographically secure random numeric codes (6–10 digits configurable) server-side when you create a verification object. The API handles token generation, storage, expiration, and delivery—you never see or manage the actual code in your backend, which eliminates implementation vulnerabilities (MessageBird Verify API docs).
What's the difference between MessageBird SMS API and Verify API?
The SMS API (
client.messages.create()) sends arbitrary text messages and requires you to manually generate OTP codes, track expiration, and manage verification attempts. The Verify API (client.verify.create()) is purpose-built for OTP/2FA, automatically handling token generation, validation, expiration, and attempt limiting. Use Verify API for authentication flows and SMS API for notifications or marketing messages.How long should OTP codes be valid?
According to NIST SP 800-63B guidelines (2023), OTP tokens should expire quickly to minimize attack windows. Recommended ranges: 5–10 minutes for standard authentication (balances security and UX), 30–60 seconds for high-security transactions (banking, medical records), 15–30 minutes for account recovery flows (users may need to check email for password reset). MessageBird Verify API supports 30 seconds to 2 days.
Can I use MessageBird Verify API with free trial accounts?
Yes, MessageBird provides free trial credits for testing, but you must use a live API key (not test mode key) for Verify API functionality. Trial accounts may have destination restrictions—verify that you can send to your test phone numbers in the MessageBird Dashboard. As of 2024, most trial accounts include €10–20 in free credits sufficient for 100–200 SMS verifications depending on destination country.
How do I prevent SMS bombing attacks on my OTP endpoint?
Implement multi-layer rate limiting: (1) Phone-number-based limits (3–5 OTP requests per number per hour) using
express-rate-limitwith customkeyGenerator, (2) IP-based limits (10 requests per IP per hour) as backup, (3) Consider CAPTCHA after 2 failed generation attempts, (4) Monitor for unusual patterns (alerts when >50 requests/hour from single source). MessageBird also applies account-level rate limits for additional protection.What happens if a user enters the wrong OTP code multiple times?
MessageBird Verify API tracks verification attempts (configurable 1–10 via
maxAttempts, default 3). After exceeding max attempts, the verification status changes to"failed"and the API returns an error on subsequent verify requests. The verification ID becomes invalid and the user must request a new OTP code. This prevents brute-force attacks (3 attempts = 0.0003% success probability with 6-digit codes).Is SMS-based OTP secure enough for sensitive applications?
SMS OTP provides moderate security suitable for most applications (account login, e-commerce checkouts, identity verification). However, NIST SP 800-63B deprecates SMS for high-security contexts due to vulnerabilities (SIM swapping, SS7 interception, phishing). For banking, healthcare, or government applications, consider: (1) App-based TOTP (Google Authenticator, Authy), (2) Hardware tokens (YubiKey), (3) MessageBird's WhatsApp verification (more secure than SMS), or (4) Multi-factor authentication combining SMS + another factor.
How do I handle users in countries with poor SMS delivery?
MessageBird Verify API supports alternative channels: (1) Voice OTP (TTS): Call user and speak code aloud (set
type: 'tts'), useful for areas with SMS blocking, (2) WhatsApp: More reliable delivery in developing markets (requires WhatsApp Business API integration), (3) Email: For non-mobile users (settype: 'email'). Monitor delivery rates by country in MessageBird Dashboard and automatically fallback to voice for countries with <95% SMS delivery success.Can I customize the OTP message template?
Yes, MessageBird Verify API allows custom templates via the
templateparameter. The string must include%tokenplaceholder which MessageBird replaces with the generated code. Example templates:"Your verification code is %token."(default),"Use code %token to log in to YourApp","%token is your one-time password (valid 5 minutes)". Keep messages under 160 characters (GSM-7) or 70 characters (Unicode) to avoid multi-part SMS charges.How do I implement OTP for user registration vs. login?
Registration flow: (1) User enters phone number during signup, (2) Send OTP via Verify API, (3) Verify code, (4) Create account with verified phone number in database. Login flow: (1) User enters username/password, (2) If credentials valid, send OTP to registered phone, (3) Verify code, (4) Issue session token/JWT. For enhanced security, require OTP on every login (2FA) or only for high-risk events (new device, unusual location, password change).
Related Resources
For comprehensive guides on SMS authentication and security: