code examples
code examples
How to Implement MessageBird OTP Verification with Node.js and Express
Learn how to build secure SMS OTP two-factor authentication using MessageBird Verify API in Node.js. Step-by-step tutorial with code examples, error handling, rate limiting, and production deployment tips.
MessageBird OTP/2FA Tutorial: Node.js + Express Implementation Guide
Build a secure One-Time Password (OTP) verification system for Two-Factor Authentication (2FA) using Node.js, Express, and the MessageBird Verify API. This step-by-step guide covers everything from project setup to deployment.
By the end of this tutorial, you'll have a functional web application that:
- Prompts users for their phone number.
- Sends an OTP via SMS using MessageBird.
- Verifies the OTP entered by the user.
You'll add a second verification factor beyond passwords, confirming users possess their registered phone number.
Important Security Context: SMS-based OTP is more vulnerable to interception than Time-based One-Time Password (TOTP) authenticator apps (per OWASP Multifactor Authentication guidelines). SMS OTP is susceptible to SIM swapping attacks (where attackers hijack your phone number by convincing carriers to transfer it to a new SIM card), SS7 protocol exploits (network-level attacks that intercept SMS messages), and man-in-the-middle attacks (where attackers intercept SMS messages in transit). For high-security applications, implement TOTP as your primary 2FA method with SMS OTP as a fallback or account recovery option.
Compliance Requirements: When implementing SMS OTP, consider these requirements:
- NIST Guidelines: NIST Special Publication 800-63B classifies SMS as a "restricted" authenticator requiring additional protections
- GDPR/Data Retention: Store phone numbers and verification logs according to GDPR requirements – collect explicit consent, implement data retention policies (typically 30–90 days for logs), and provide deletion mechanisms
- Telecom Regulations: Comply with regional SMS regulations like TCPA (US), CASL (Canada), and ePrivacy Directive (EU) for marketing messages
Technologies You'll Use:
- Node.js: JavaScript runtime environment
- Express.js: Minimal Node.js web application framework
- MessageBird Node.js SDK (v4.0.1): Interact with the MessageBird Verify API (Note: Last updated in 2021; verify compatibility with your Node.js version)
- Handlebars: Simple templating engine for HTML views
- dotenv: Manage environment variables securely
- body-parser: Parse incoming request bodies
Prerequisites:
- Node.js and npm (or yarn) installed
- MessageBird account with a Live API Key (Test keys don't work with the Verify API)
- Phone number capable of receiving SMS messages for testing
- Basic understanding of Node.js, Express, and asynchronous JavaScript
- Cost Consideration: MessageBird charges per SMS (~$0.008 per message for US numbers as of 2024; rates vary by country). Ensure your account has sufficient prepaid balance.
Cost Estimates by Region:
| Region | Cost per SMS | 100 Verifications | 1,000 Verifications |
|---|---|---|---|
| US/Canada | $0.008 | $0.80 | $8.00 |
| UK | $0.063 | $6.30 | $63.00 |
| Western Europe | $0.065 | $6.50 | $65.00 |
| Asia-Pacific | $0.015–0.080 | $1.50–8.00 | $15.00–80.00 |
System Architecture:
The flow involves three main components:
- User's Browser: Interacts with the web application, submits phone number and OTP
- Node.js/Express Server: Handles HTTP requests, manages application logic, interacts with MessageBird API, and renders HTML views
- MessageBird Verify API: Generates and sends OTP via SMS, verifies user-submitted tokens
Request Flow:
- User submits phone number → Server validates format → MessageBird creates verification and sends SMS
- MessageBird returns verification ID → Server stores ID and prompts for OTP
- User submits OTP → Server sends ID + token to MessageBird → MessageBird validates and returns success/failure
1. Set Up Your Node.js Project Environment
Create your project directory and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project:
bashmkdir node-messagebird-otp cd node-messagebird-otp -
Initialize Node.js Project: Initialize the project using npm (the
-yflag accepts default settings):bashnpm init -y -
Install Dependencies: Install Express, MessageBird SDK, Handlebars, dotenv, and body-parser:
bashnpm install express messagebird express-handlebars dotenv body-parserOptional but Recommended: For robust phone number validation, install
libphonenumber-js:bashnpm install libphonenumber-jsExample package.json:
json{ "name": "node-messagebird-otp", "version": "1.0.0", "dependencies": { "express": "^4.18.2", "messagebird": "^4.0.1", "express-handlebars": "^7.1.2", "dotenv": "^16.3.1", "body-parser": "^1.20.2", "libphonenumber-js": "^1.10.51" } } -
Create Project Structure: Set up your directory structure:
bashmkdir views mkdir views/layouts touch index.js touch .env touch .env.example touch .gitignore touch views/layouts/main.handlebars touch views/step1.handlebars touch views/step2.handlebars touch views/step3.handlebarsYour structure should look like this:
node-messagebird-otp/ ├── node_modules/ ├── views/ │ ├── layouts/ │ │ └── main.handlebars │ ├── step1.handlebars │ ├── step2.handlebars │ └── step3.handlebars ├── .env ├── .env.example ├── .gitignore ├── index.js ├── package-lock.json └── package.json -
Configure Environment Variables: Never hardcode your MessageBird API key. Use
dotenvto load it from a.envfile.-
Get Your MessageBird Live API Key:
- Log in to your MessageBird Dashboard.
- Navigate to Developers in the left-hand menu.
- Click the API access tab.
- Create a Live key if you don't have one. Copy it. (The Verify API requires a live key.)
-
Add the Key to
.env: Open.envand add:dotenvMESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEYReplace
YOUR_LIVE_API_KEYwith your actual key. -
Add a Template to
.env.example: Create an example file without exposing secrets:dotenvMESSAGEBIRD_API_KEY=your_messagebird_live_api_key_here -
Create
.gitignore: Add.envto your.gitignorefile to prevent committing secrets:gitignore# Environment variables .env # Dependencies node_modules/ # Logs *.log npm-debug.log* # OS files .DS_Store Thumbs.db
-
2. Build the Express Application with MessageBird Integration
Build the Express application logic in index.js and create the corresponding Handlebars views.
-
Set Up
index.js: Openindex.jsand add the initial setup:javascript// index.js // 1. Import dependencies const express = require('express'); const exphbs = require('express-handlebars'); const bodyParser = require('body-parser'); require('dotenv').config(); // Load environment variables // 2. Initialize MessageBird SDK const messagebirdApiKey = process.env.MESSAGEBIRD_API_KEY; if (!messagebirdApiKey) { console.error("Error: MESSAGEBIRD_API_KEY is not set in environment variables."); process.exit(1); // Exit if API key is missing } const messagebird = require('messagebird')(messagebirdApiKey); // 3. Initialize Express app and middleware const app = express(); app.engine('handlebars', exphbs.engine({ defaultLayout: 'main' })); // For express-handlebars v6+ app.set('view engine', 'handlebars'); app.use(bodyParser.urlencoded({ extended: true })); // Parse URL-encoded bodies // 4. Define port const PORT = process.env.PORT || 3000; // --- Routes will be added below --- // 5. Start the server app.listen(PORT, () => { console.log(`Server listening on http://localhost:${PORT}`); });- Why call
dotenv.config()first? Load environment variables early before using them in the MessageBird SDK initialization. - Why use
bodyParser? Parse form data (application/x-www-form-urlencoded) and make it available inreq.body. - Why use
express-handlebars? Dynamically generate HTML pages with templates, separating presentation from logic.
Version Compatibility: If you're using
express-handlebarsv5 or earlier, useexphbs({ defaultLayout: 'main' })instead ofexphbs.engine({ defaultLayout: 'main' }). Check your installed version withnpm list express-handlebars. - Why call
-
Create Main Layout (
views/layouts/main.handlebars): Define the basic HTML structure shared by all pages:handlebars<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>MessageBird OTP Verification</title> <style> body { font-family: sans-serif; padding: 20px; } .container { max-width: 500px; margin: auto; border: 1px solid #ccc; padding: 20px; border-radius: 8px; } h1 { text-align: center; color: #0575E6; } /* MessageBird blue */ label, input { display: block; margin-bottom: 10px; width: 95%; } input[type="tel"], input[type="text"] { padding: 10px; border: 1px solid #ccc; border-radius: 4px; } input[type="submit"] { background-color: #0575E6; color: white; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; width: auto; } input[type="submit"]:hover { background-color: #045db2; } .error { color: #d32f2f; border: 1px solid #d32f2f; padding: 10px; margin-bottom: 15px; border-radius: 4px; background-color: #ffebeb; } .success { color: #388e3c; border: 1px solid #388e3c; padding: 10px; margin-bottom: 15px; border-radius: 4px; background-color: #e6ffed; } p { line-height: 1.6; } </style> </head> <body> <div class="container" role="main"> <h1>MessageBird OTP Verification</h1> </div> </body> </html>Accessibility improvements: Added
lang="en"attribute,role="main"for screen readers, and improved color contrast for error/success messages. -
Step 1: Request Phone Number (
views/step1.handlebars) Display the form for users to enter their phone number:handlebars<h2>Step 1: Enter Your Phone Number</h2> <div class="error" role="alert" aria-live="polite"></div> <p>Enter your phone number in international format (e.g., +14155552671) to receive a verification code via SMS.</p> <form method="post" action="/send-otp"> <label for="number">Phone Number:</label> <input type="tel" id="number" name="number" required placeholder="+14155552671" aria-required="true" aria-describedby="phone-hint" /> <small id="phone-hint">Include country code with + prefix</small> <input type="submit" value="Send Code" /> </form>{{#if error}}: Conditionally displays an error message if theerrorvariable is passed to the template.action="/send-otp": Form data is sent via HTTP POST to the/send-otproute.type="tel": Mobile browsers display a numeric keypad.- Accessibility: Added
role="alert",aria-live="polite",aria-required="true", andaria-describedbyfor screen readers.
-
Add Route for Step 1 (GET
/) Add this route toindex.jsbefore theapp.listen()call:javascript// index.js (inside the routes section) // Display the initial form (Step 1) app.get('/', (req, res) => { res.render('step1'); // Renders views/step1.handlebars }); -
Step 2: Send OTP and Request Code (
views/step2.handlebars) Ask the user to enter the OTP they received. Include a hidden field to pass the MessageBird verification ID.handlebars<h2>Step 2: Enter Verification Code</h2> <div class="error" role="alert" aria-live="polite"></div> <p>We sent a verification code via SMS to your phone number.</p> <p>Enter the 6-digit code below:</p> <form method="post" action="/verify-otp"> <input type="hidden" name="id" value="" /> <label for="token">Verification Code:</label> <input type="text" id="token" name="token" required pattern="\d{6}" title="Enter the 6-digit code" aria-required="true" inputmode="numeric" /> <input type="submit" value="Verify Code" /> </form> <p><a href="/">Try a different number?</a></p>input type="hidden" name="id": Stores the verification ID from MessageBird. The server needs it to link the token back to the original request, but users don't need to see it.pattern="\d{6}": Basic HTML5 validation for a 6-digit number.- Security Note: Hidden fields are vulnerable to manipulation. For production applications with user sessions, store the verification ID server-side in the session instead of passing it via hidden fields. This prevents users from tampering with the ID.
-
Add Route for Sending OTP (POST
/send-otp) Handle phone number submission, call the MessageBird API to send the OTP, and render the code entry form. Add this toindex.js:javascript// index.js (inside the routes section) // Handle phone number submission and send OTP (Step 2) app.post('/send-otp', (req, res) => { const number = req.body.number; // Basic validation (use libphonenumber-js for production) if (!number || !/^\+[1-9]\d{1,14}$/.test(number)) { return res.render('step1', { error: 'Enter a valid phone number in international format (e.g., +14155552671).' }); } // Enhanced validation using libphonenumber-js (recommended for production) // Uncomment if you installed libphonenumber-js: // const { isValidPhoneNumber } = require('libphonenumber-js'); // if (!isValidPhoneNumber(number)) { // return res.render('step1', { error: 'Enter a valid phone number in international format.' }); // } const params = { originator: 'VerifyApp', // Your app name (max 11 alphanumeric chars) or verified number template: 'Your verification code is %token.', // Message template // type: 'sms', // Default: sms; use 'tts' for voice call // timeout: 600, // Token validity in seconds (default: 600 = 10 minutes) // tokenLength: 6, // OTP length (default: 6) }; console.log(`Sending verification to ${number}`); messagebird.verify.create(number, params, (err, response) => { if (err) { // Handle API errors console.error("MessageBird API Error:", err); let userErrorMessage = 'Failed to send verification code. Try again later.'; if (err.errors && err.errors.length > 0) { const firstError = err.errors[0]; if (firstError.code === 21) { // Invalid recipient userErrorMessage = 'The phone number is invalid or not reachable.'; } else { userErrorMessage = `Error: ${firstError.description}`; } } return res.render('step1', { error: userErrorMessage }); } // Successfully initiated verification console.log("Verification Response:", response); // Render step 2, pass the verification ID res.render('step2', { id: response.id }); }); });- Input Validation: Includes basic regex for international format. Use
libphonenumber-jsin production for accurate E.164 validation. messagebird.verify.create(number, params, callback): Core API call.number: Recipient's phone number in E.164 format (e.g., +14155552671)params: Configuration optionsoriginator: Sender ID displayed on user's phone. Must be alphanumeric (max 11 chars) or verified MessageBird number. Important: Alphanumeric originators work in Europe but not in US/Canada. For North America, use a purchased MessageBird virtual number or shortcode.template: Message text.%tokenis replaced by the OTP.timeout: Token validity in seconds. Default: 600 seconds (10 minutes), not 30 seconds. Adjust for your use case.tokenLength: OTP length. Default: 6 digits.
callback(err, response): Handles async responseerr: Error occurred (invalid number, API key issue, insufficient balance). Log detailed error and show user-friendly message.response: Request successful.response.idis the unique identifier for this verification attempt.
Production validation example with libphonenumber-js:
javascriptconst { parsePhoneNumber, isValidPhoneNumber } = require('libphonenumber-js'); try { const phoneNumber = parsePhoneNumber(number); if (!isValidPhoneNumber(number) || !phoneNumber.isPossible()) { return res.render('step1', { error: 'Enter a valid phone number in international format.' }); } // Use phoneNumber.formatInternational() to ensure E.164 format const formattedNumber = phoneNumber.format('E.164'); } catch (error) { return res.render('step1', { error: 'Invalid phone number format.' }); }MessageBird API Error Codes:
Code Description Solution 2 Request not allowed Check API key permissions 9 Missing or invalid parameters Validate all required fields 20 Verification ID not found ID expired or already verified 21 Invalid recipient number Validate E.164 format 23 Invalid token Token incorrect or expired 25 Too many verification attempts Implement rate limiting - Input Validation: Includes basic regex for international format. Use
-
Step 3: Verification Result (
views/step3.handlebars) Show success message upon correct verification:handlebars<h2>Step 3: Verification Successful!</h2> <div class="success" role="status" aria-live="polite"> <p>Your phone number has been successfully verified.</p> </div> <h3>Next Steps:</h3> <ul> <li>Your phone number is now linked to your account</li> <li>You'll receive SMS notifications for important account activities</li> <li>Enable 2FA in your account settings for additional security</li> </ul> <p><a href="/">Start Over</a></p>Integration Example: In production, mark the phone number as verified in your database:
javascript// After successful verification in /verify-otp route: await db.users.update( { userId: req.session.userId }, { phoneNumberVerified: true, phoneNumber: verifiedNumber } ); -
Add Route for Verifying OTP (POST
/verify-otp) Take the verification ID and user-entered token, call MessageBird to verify them, and render success or failure. Add this toindex.js:javascript// index.js (inside the routes section) // Handle OTP submission and verify (Step 3) app.post('/verify-otp', (req, res) => { const id = req.body.id; const token = req.body.token; if (!id || !token) { return res.render('step1', { error: 'Missing verification ID or token.' }); } console.log(`Verifying token ${token} for ID ${id}`); messagebird.verify.verify(id, token, (err, response) => { if (err) { // Verification failed (invalid token, expired, etc.) console.error("Verification Error:", err); let userErrorMessage = 'Verification failed. Check the code and try again.'; if (err.errors && err.errors.length > 0) { const firstError = err.errors[0]; // Error code 20: "verify request not found or already verified" // Error code 23: "token is invalid" if (firstError.code === 23 || (firstError.description && firstError.description.includes('expired'))) { userErrorMessage = 'The code is incorrect or expired. Request a new code.'; } else { userErrorMessage = `Error: ${firstError.description}`; } } // Re-render step 2 with error, pass the original ID back return res.render('step2', { error: userErrorMessage, id: id }); } // Verification successful! console.log("Verification Success Response:", response); res.render('step3'); }); });messagebird.verify.verify(id, token, callback): Key API callid: Verification ID from thecreatecall (passed via hidden form field)token: OTP entered by usercallback(err, response):err: Token was incorrect, expired, or ID was invalid. Renderstep2with error message, passidback so user can retry.response: Token correct! Render success page. In production, mark the user's phone number as verified in your database.
3. Implement Error Handling and Logging Best Practices
Implement robust error handling and logging:
- API Errors: Catch errors from
messagebird.verify.createandmessagebird.verify.verify. Log detailed errors to console (console.error) for debugging. Display user-friendly messages on the appropriate page. Check specific error codes (21 for invalid recipient, 23 for invalid token) to provide better feedback. - Input Validation: Basic validation for phone number format in
/send-otp. - Logging: Track flow and errors with
console.logandconsole.error. For production, use a dedicated logging library (Winston or Pino) to structure logs, write to files, and set log levels.
Winston configuration example:
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console({ format: winston.format.simple() })
]
});
// Replace console.log with logger.info() and console.error with logger.error()Error Monitoring: Integrate error monitoring services for production:
- Sentry: Captures exceptions and provides stack traces
- Datadog: Application performance monitoring with error tracking
- AWS CloudWatch: Native AWS logging and monitoring
4. Security Best Practices for OTP Implementation
Protect your application and users:
-
API Key Security: Load API key from
.envand never commit to version control. Ensure.envis in.gitignore. In production, use your platform's secure environment variable management. -
Input Validation: Sanitize and validate all user inputs. Use
libphonenumber-js(lighter than Google's libphonenumber) for robust E.164 format checking. Validate token format (numeric, expected length). -
Rate Limiting: Protect
/send-otpfrom abuse. Repeated code requests cost money and harass users. Implement rate limiting per IP or user account withexpress-rate-limit. MessageBird limits 3 verification attempts per ID (maxAttempts: 3), but add application-level rate limiting. Consider stricter limits on/verify-otpto prevent token guessing attacks.Install rate limiting:
bashnpm install express-rate-limitConfigure rate limiting:
javascript// index.js (add with other requires/middleware) const rateLimit = require('express-rate-limit'); const otpLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Limit each IP to 5 requests per window message: 'Too many requests. Try again after 15 minutes.', standardHeaders: true, // Return rate limit info in headers legacyHeaders: false, // Disable legacy headers }); // Apply to OTP sending route app.use('/send-otp', otpLimiter); // Optional: Add stricter limiter for /verify-otp // const verifyLimiter = rateLimit({ windowMs: 5 * 60 * 1000, max: 10, message: 'Too many verification attempts.' }); // app.use('/verify-otp', verifyLimiter);Per-phone-number rate limiting example:
javascriptconst phoneLimitStore = new Map(); function checkPhoneRateLimit(phoneNumber) { const now = Date.now(); const phoneData = phoneLimitStore.get(phoneNumber) || { count: 0, resetTime: now + 3600000 }; if (now > phoneData.resetTime) { phoneData.count = 0; phoneData.resetTime = now + 3600000; // Reset after 1 hour } phoneData.count++; phoneLimitStore.set(phoneNumber, phoneData); return phoneData.count <= 3; // Max 3 attempts per hour per phone } -
HTTPS: Always use HTTPS in production to encrypt client-server communication.
-
Session Management: For real applications with user accounts, use secure session management. Don't rely solely on hidden form fields for sensitive state like verification IDs. Store server-side in user's session.
Secure session storage options:
- Redis: Fast, scalable, ideal for distributed systems
- Database: PostgreSQL, MySQL – persistent storage
- express-session with connect-redis:
javascriptconst session = require('express-session'); const RedisStore = require('connect-redis')(session); const redis = require('redis'); const redisClient = redis.createClient(); app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 3600000 } })); -
CSRF Protection: Implement CSRF protection to prevent cross-site request forgery attacks:
bashnpm install csurfjavascriptconst csrf = require('csurf'); const csrfProtection = csrf({ cookie: true }); app.use(require('cookie-parser')()); app.use(csrfProtection); // Pass CSRF token to forms app.get('/', (req, res) => { res.render('step1', { csrfToken: req.csrfToken() }); }); -
Recovery Codes: Provide users with recovery codes (single-use backup codes) for production 2FA in case they lose phone access. Store hashed in your database.
5. Test Your OTP Application
Verify everything works correctly:
-
Start the Application: Ensure
.envhas the correctMESSAGEBIRD_API_KEY. Run from your terminal:bashnode index.jsYou should see
Server listening on http://localhost:3000. -
Manual Browser Testing:
- Open
http://localhost:3000in your browser. - You should see "Step 1: Enter Your Phone Number."
- Enter your phone number in international format (e.g.,
+1XXXYYYZZZZ). Click "Send Code." - On success, you'll see "Step 2: Enter Verification Code." Check your phone for the SMS (may take a few seconds).
- Enter the 6-digit code. Click "Verify Code."
- If correct and not expired, you'll see "Step 3: Verification Successful!"
- Test Error Cases: Try invalid format, incorrect OTP, or wait past timeout (default 10 minutes) to see error messages.
- Open
-
Automated Testing: Implement unit and integration tests with Jest:
bashnpm install --save-dev jest supertestExample test (test/otp.test.js):
javascriptconst request = require('supertest'); const app = require('../index'); // Export your app describe('OTP Flow', () => { it('should render step 1 page', async () => { const response = await request(app).get('/'); expect(response.status).toBe(200); expect(response.text).toContain('Enter Your Phone Number'); }); it('should reject invalid phone numbers', async () => { const response = await request(app) .post('/send-otp') .send({ number: 'invalid' }); expect(response.text).toContain('valid phone number'); }); });Mock Testing (without SMS credits): Use MessageBird's test mode or mock the SDK:
javascriptjest.mock('messagebird', () => { return () => ({ verify: { create: (number, params, callback) => { callback(null, { id: 'mock-id-12345' }); }, verify: (id, token, callback) => { if (token === '123456') { callback(null, { id: id, status: 'verified' }); } else { callback({ errors: [{ code: 23, description: 'Invalid token' }] }); } } } }); }); -
API Endpoint Testing (Optional – using
curl): Test POST endpoints directly.-
Send OTP:
bash# Replace +1XXXYYYZZZZ with valid number curl -X POST http://localhost:3000/send-otp \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "number=+1XXXYYYZZZZ" \ --verboseLook for HTML response with Step 2 form and hidden
idfield. Extract theidvalue. -
Verify OTP:
bash# Replace YOUR_VERIFICATION_ID and RECEIVED_OTP curl -X POST http://localhost:3000/verify-otp \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "id=YOUR_VERIFICATION_ID&token=RECEIVED_OTP" \ --verboseLook for HTML response for Step 3 (success) or Step 2 (failure).
-
6. Enhance Your Implementation
Improve security and user experience:
-
Integrate with User Accounts: Store verification status (
isPhoneNumberVerified: true) in your database upon success.Database schema example (PostgreSQL):
sqlCREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, phone_number VARCHAR(20), phone_verified BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE verification_logs ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), phone_number VARCHAR(20), verification_id VARCHAR(255), status VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE recovery_codes ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), code_hash VARCHAR(255), used BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -
Session Management: Store
verificationIdserver-side in session instead of hidden field for better security with logged-in users.javascriptapp.post('/send-otp', (req, res) => { // ... validation code ... messagebird.verify.create(number, params, (err, response) => { if (!err) { req.session.verificationId = response.id; req.session.phoneNumber = number; res.render('step2'); } }); }); app.post('/verify-otp', (req, res) => { const id = req.session.verificationId; const token = req.body.token; // ... verification code ... }); -
Voice (TTS) OTP: Set
params.typeto'tts'inmessagebird.verify.createto send OTP via voice call instead of SMS.javascriptconst params = { originator: 'VerifyApp', template: 'Your verification code is %token.', type: 'tts', // Voice call instead of SMS language: 'en-us', voice: 'male' }; -
Customization: Explore
verify.createparameters:timeout(default 600s = 10 minutes),tokenLength(default 6),language, andvoice(for TTS). -
UI/UX: Improve interface and provide clearer feedback.
-
Resend Code: Allow users to request codes again after a delay.
javascriptapp.post('/resend-otp', otpLimiter, (req, res) => { const id = req.session.verificationId; const number = req.session.phoneNumber; // Check if 60 seconds have passed since last send if (req.session.lastOtpSent && Date.now() - req.session.lastOtpSent < 60000) { return res.render('step2', { error: 'Please wait 60 seconds before requesting a new code.', id: id }); } // Send new OTP with same logic as /send-otp req.session.lastOtpSent = Date.now(); // ... MessageBird API call ... }); -
TOTP Implementation: Implement TOTP (Time-based One-Time Password) with authenticator apps as primary 2FA, using SMS as fallback for higher security.
7. Troubleshoot Common Issues
Solve common problems:
- Invalid API Key: Verify
MESSAGEBIRD_API_KEYin.envis correct and is a Live key. Check for typos or spaces. Error messages mention authentication failure. - Invalid Phone Number: MessageBird requires E.164 international format (e.g.,
+14155552671). Include '+' and country code. API error code 21 indicates this. Uselibphonenumber-jsfor production validation. - Originator Restrictions: Alphanumeric sender IDs (e.g., 'VerifyApp') work in Europe but not in US/Canada. For North America, use a purchased MessageBird virtual number or shortcode. Test with your MessageBird number if alphanumeric fails.
- Token Expired/Invalid: Tokens are valid for
timeoutduration (default 600 seconds = 10 minutes, not 30 seconds). Verification fails if users take too long (error code 20 or 23). Ensure entered token matches received code. MessageBird allows up to 3 attempts per ID (maxAttempts: 3). - Message Delivery Issues: SMS delivery can be delayed or fail due to carrier issues. Implement "Resend Code" option or offer Voice OTP as fallback. Check MessageBird dashboard under Logs → SMS for delivery reports showing status (delivered, failed, pending).
- Rate Limits: Sending too many requests quickly causes temporary blocks. Implement client-side and server-side rate limiting (see Security section).
- MessageBird Balance: Ensure sufficient prepaid balance. SMS OTP costs ~$0.008 per message (US), prices vary by country. Check balance in MessageBird dashboard under Overview.
- SDK Compatibility: MessageBird Node.js SDK (v4.0.1) was last updated in 2021. Verify compatibility with your Node.js version. Check for community forks or alternatives if issues arise.
Diagnostic Checklist:
- ☐ Check
MESSAGEBIRD_API_KEYis set correctly in.env - ☐ Verify phone number includes '+' and country code
- ☐ Confirm MessageBird account has sufficient balance
- ☐ Check MessageBird dashboard logs for delivery status
- ☐ Verify originator settings for your target region
- ☐ Test with a different phone number
- ☐ Check console logs for detailed error messages
- ☐ Verify Node.js version compatibility
8. Deploy to Production
Prepare your application for deployment:
-
Environment Variables: Never hardcode API keys. Use your platform's environment variable management (Heroku Config Vars, AWS Secrets Manager, Docker environment variables). Don't deploy
.envfiles.Platform-specific guides:
- Heroku:
heroku config:set MESSAGEBIRD_API_KEY=your_key - AWS: Use AWS Secrets Manager or Systems Manager Parameter Store
- DigitalOcean: Set environment variables in App Platform settings
- Docker: Pass via
docker run -e MESSAGEBIRD_API_KEY=your_keyor docker-compose
- Heroku:
-
HTTPS: Configure HTTPS using Nginx or your platform's load balancer.
-
Process Management: Use PM2 or Nodemon (development) to keep your application running reliably and handle restarts.
bashnpm install -g pm2 pm2 start index.js --name messagebird-otp pm2 startup pm2 save -
Logging: Configure robust logging for production monitoring.
-
Dependencies: List all production dependencies in
package.json(notdevDependencies). Runnpm install --productionin deployment environment.
Docker deployment example:
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- MESSAGEBIRD_API_KEY=${MESSAGEBIRD_API_KEY}
- NODE_ENV=production
restart: unless-stoppedFrequently Asked Questions About MessageBird OTP/2FA
How do I implement OTP verification in Node.js?
Implement OTP verification in Node.js using the MessageBird Verify API. Install the messagebird SDK, initialize the client with your API key, create a verification request with client.verify.create(), and verify user input with client.verify.verify(). The process includes sending an SMS with a random token and validating the user's response within the timeout period (default 600 seconds).
What is MessageBird Verify API?
MessageBird Verify API is a service that handles OTP (One-Time Password) generation, delivery, and verification for two-factor authentication. It automatically generates random tokens, sends them via SMS, manages token expiration, and provides verification endpoints. The API handles rate limiting, attempt tracking, and security features to protect against brute-force attacks.
Is SMS OTP secure for 2FA?
SMS OTP provides moderate security for two-factor authentication but has known vulnerabilities. According to OWASP Multifactor Authentication guidelines, SMS is susceptible to SIM swapping attacks, SS7 protocol exploits, and device theft. For high-security applications, use TOTP (Time-based One-Time Password) apps like Google Authenticator or hardware tokens. SMS OTP works best for low-to-medium security scenarios when combined with rate limiting and account monitoring.
Mitigation strategies:
- Implement account activity alerts via email
- Require additional identity verification for SIM changes
- Use TOTP as primary method with SMS as fallback
- Monitor for suspicious login patterns
- Implement device fingerprinting
How much does MessageBird SMS cost?
MessageBird SMS pricing varies by destination country. As of 2024, US SMS costs approximately $0.008 per message. International rates differ significantly – check MessageBird's pricing page for your target regions. The Verify API uses standard SMS pricing with no additional verification fees. Calculate costs based on your expected verification volume and user geography.
What's the difference between SMS OTP and TOTP?
SMS OTP delivers codes via text message to your phone number, while TOTP (Time-based One-Time Password) generates codes locally using an authenticator app. TOTP is more secure because it doesn't rely on telecom networks vulnerable to interception. SMS OTP offers better user experience since it requires no app installation. Use TOTP for high-security applications and SMS OTP for user convenience in lower-risk scenarios.
How do I handle MessageBird API errors in Node.js?
Handle MessageBird API errors using try-catch blocks around API calls. Check for specific error codes in the response: error code 2 indicates permission issues, code 9 means missing parameters, code 20 indicates an invalid verification ID, code 21 means the recipient number is invalid, code 23 indicates an invalid token, and code 25 indicates too many attempts. Always provide user-friendly error messages and implement logging for debugging. Use proper HTTP status codes (400 for validation errors, 500 for server errors, 429 for rate limiting).
Can I customize the OTP message template in MessageBird?
Yes, customize the OTP message template using the template parameter in client.verify.create(). Include %token as a placeholder where the OTP code appears. Keep messages under 160 characters to avoid SMS concatenation. Example: "Your VerifyApp code is %token. Valid for 10 minutes." Alphanumeric sender IDs work in Europe but not in the US or Canada, where you need a registered long code.
How long should OTP codes remain valid?
Set OTP validity between 5–10 minutes (300–600 seconds) using the timeout parameter. MessageBird's default is 600 seconds (10 minutes). Shorter timeouts improve security but may frustrate users with delayed messages. Longer timeouts reduce user friction but increase vulnerability to replay attacks. Consider your user experience needs and security requirements when setting this value.
How do I implement rate limiting for OTP requests?
Implement rate limiting using express-rate-limit middleware. Limit OTP generation to 3–5 requests per hour per IP address and per phone number. Set verification attempt limits to 3 tries per verification ID (MessageBird's default). Store rate limit data in Redis for distributed systems or use in-memory storage for single-server deployments. Return HTTP 429 (Too Many Requests) when limits are exceeded.
What happens if a user doesn't receive the OTP SMS?
If a user doesn't receive the OTP SMS, check MessageBird's delivery status using the verification ID. Common causes include invalid phone numbers, carrier filtering, or network delays. Implement a "resend code" feature with rate limiting (maximum 2–3 resends per verification attempt). Provide alternative verification methods like voice calls or email as fallbacks. Log delivery failures for monitoring and troubleshooting.
9. Complete Code Repository
A complete, runnable version of this project is available on GitHub at github.com/your-username/messagebird-node-otp-example.
You now have a functional Node.js application for SMS OTP verification using MessageBird's Verify API. This provides a solid foundation for adding two-factor authentication to your web applications. Adapt the error handling, security measures, and UI/UX to fit your production environment's specific needs.
Frequently Asked Questions
How to set up MessageBird OTP in Node.js?
To set up MessageBird OTP in Node.js, you'll need to install necessary dependencies like Express, the MessageBird SDK, Handlebars, dotenv, and body-parser. Create the project structure, set up your .env file with your MessageBird API key, and then implement the core logic within index.js and your Handlebars view templates as described in the guide. This allows for user interaction to send and verify the OTP codes via SMS through the MessageBird API and an Express server.
What is the MessageBird Verify API used for?
The MessageBird Verify API is used for generating and sending One-Time Passwords (OTPs) via SMS, commonly for Two-Factor Authentication (2FA). It allows you to securely verify a user's phone number by sending a unique code and then verifying it, enhancing your app's security by adding a second verification factor beyond a password.
Why does MessageBird OTP require a live API key?
MessageBird OTP requires a live API key because it involves sending real SMS messages to users' phones, which incurs costs. Test API keys don't have access to the SMS functionality needed for the Verify API. You can find your live API key in the "Developers" section, "API access" tab, within your MessageBird Dashboard. Create one if you haven't already.
When should I use Two-Factor Authentication with MessageBird?
Two-Factor Authentication with MessageBird is beneficial when you want to strengthen the security of your applications, especially during sensitive actions like login, account updates, or financial transactions. Adding 2FA helps protect against unauthorized access, even if a user's password is compromised.
How to send OTP with MessageBird API?
You can send OTPs with the MessageBird API by making a POST request to `/send-otp` route with the user's phone number in international format. Ensure your backend is set up with the MessageBird Node.js SDK and uses `messagebird.verify.create()` with the user's number and message template containing `%token` placeholder. A unique verification ID is generated and returned in the API's response which is then used to verify the entered OTP.
What are the MessageBird OTP prerequisites?
Prerequisites for MessageBird OTP integration include installed Node.js and npm (or yarn), a MessageBird account with a live API key, a phone number capable of receiving SMS for testing, and basic understanding of Node.js, Express.js, and asynchronous JavaScript.
What is the architecture of MessageBird OTP system?
The MessageBird OTP system uses a three-part architecture involving the user's browser, your Node.js/Express server, and the MessageBird Verify API. The browser interacts with the server for phone number and OTP submission, the server handles requests and interacts with the API using the SDK, and the MessageBird API generates, sends, and verifies the OTP.
How to implement error handling for MessageBird Verify API?
Implement error handling by checking for errors returned by the `messagebird.verify.create` and `messagebird.verify.verify` functions. Use `console.error` for logging detailed errors, then provide helpful messages to the user on the UI based on the error codes. This tutorial demonstrates handling errors for invalid numbers, API issues, and incorrect OTPs, improving user experience.
How to improve security of MessageBird OTP?
Enhance OTP security by using environment variables for API keys, implementing robust phone number and token validation, adding rate limiting to the /send-otp route (and potentially /verify-otp), always using HTTPS in production, and implementing secure session management.
How to verify OTP with MessageBird?
To verify the OTP, the user enters the code they received via SMS. The backend takes this user-entered token along with the verification ID (generated when sending the initial request) and calls the `messagebird.verify.verify(id, token, callback)` function. If successful, the callback renders a success page, and the user's phone number is marked as verified.
Can I customize the MessageBird OTP message?
Yes, you can customize the MessageBird OTP message by providing a custom template with the `template` parameter in `messagebird.verify.create()`. The `%token` placeholder within the template is replaced with the actual OTP, allowing flexibility in wording and branding.
How to test my MessageBird OTP integration?
You can test your integration by running the application locally with `node index.js` and manually interacting with it in your browser. You should be able to submit your phone number, receive an OTP via SMS, and then submit the OTP for verification. Alternatively, test with `curl` by sending POST requests to `/send-otp` and `/verify-otp` endpoints with appropriate parameters.
Why is my MessageBird OTP not working?
Check for common issues such as incorrect or test API keys, phone numbers not in E.164 format, originator restrictions, expired or invalid tokens, message delivery issues, and rate limits. Ensure your MessageBird account has a sufficient balance for sending messages. Review the troubleshooting section of the article.
What are MessageBird deployment considerations?
Deployment considerations include managing environment variables securely, enforcing HTTPS, utilizing a process manager like PM2, configuring a production-ready logging solution, and ensuring all production dependencies are properly installed.
How to integrate MessageBird OTP with user accounts?
To integrate with user accounts, after successful verification, store a flag (e.g., `isPhoneNumberVerified: true`) in your user database. Ideally, the `verificationId` should also be stored in a server-side session during the process for enhanced security if tied to a logged-in user, rather than relying on hidden form fields.