code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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:

  1. Prompts users for their phone number.
  2. Sends an OTP via SMS using MessageBird.
  3. 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:

RegionCost per SMS100 Verifications1,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:

  1. User's Browser: Interacts with the web application, submits phone number and OTP
  2. Node.js/Express Server: Handles HTTP requests, manages application logic, interacts with MessageBird API, and renders HTML views
  3. MessageBird Verify API: Generates and sends OTP via SMS, verifies user-submitted tokens

Request Flow:

  1. User submits phone number → Server validates format → MessageBird creates verification and sends SMS
  2. MessageBird returns verification ID → Server stores ID and prompts for OTP
  3. 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.

  1. Create Project Directory: Open your terminal and create a new directory for the project:

    bash
    mkdir node-messagebird-otp
    cd node-messagebird-otp
  2. Initialize Node.js Project: Initialize the project using npm (the -y flag accepts default settings):

    bash
    npm init -y
  3. Install Dependencies: Install Express, MessageBird SDK, Handlebars, dotenv, and body-parser:

    bash
    npm install express messagebird express-handlebars dotenv body-parser

    Optional but Recommended: For robust phone number validation, install libphonenumber-js:

    bash
    npm install libphonenumber-js

    Example 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"
      }
    }
  4. Create Project Structure: Set up your directory structure:

    bash
    mkdir 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.handlebars

    Your 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
  5. Configure Environment Variables: Never hardcode your MessageBird API key. Use dotenv to load it from a .env file.

    • Get Your MessageBird Live API Key:

      1. Log in to your MessageBird Dashboard.
      2. Navigate to Developers in the left-hand menu.
      3. Click the API access tab.
      4. 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 .env and add:

      dotenv
      MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY

      Replace YOUR_LIVE_API_KEY with your actual key.

    • Add a Template to .env.example: Create an example file without exposing secrets:

      dotenv
      MESSAGEBIRD_API_KEY=your_messagebird_live_api_key_here
    • Create .gitignore: Add .env to your .gitignore file 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.

  1. Set Up index.js: Open index.js and 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 in req.body.
    • Why use express-handlebars? Dynamically generate HTML pages with templates, separating presentation from logic.

    Version Compatibility: If you're using express-handlebars v5 or earlier, use exphbs({ defaultLayout: 'main' }) instead of exphbs.engine({ defaultLayout: 'main' }). Check your installed version with npm list express-handlebars.

  2. Create Main Layout (views/layouts/main.handlebars): Define the basic HTML structure shared by all pages:

    handlebars
    {{!-- views/layouts/main.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>
            {{{body}}}
        </div>
    </body>
    </html>

    Accessibility improvements: Added lang="en" attribute, role="main" for screen readers, and improved color contrast for error/success messages.

  3. Step 1: Request Phone Number (views/step1.handlebars) Display the form for users to enter their phone number:

    handlebars
    {{!-- views/step1.handlebars --}}
    <h2>Step 1: Enter Your Phone Number</h2>
    
    {{#if error}}
        <div class="error" role="alert" aria-live="polite">{{error}}</div>
    {{/if}}
    
    <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 the error variable is passed to the template.
    • action="/send-otp": Form data is sent via HTTP POST to the /send-otp route.
    • type="tel": Mobile browsers display a numeric keypad.
    • Accessibility: Added role="alert", aria-live="polite", aria-required="true", and aria-describedby for screen readers.
  4. Add Route for Step 1 (GET /) Add this route to index.js before the app.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
    });
  5. 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
    {{!-- views/step2.handlebars --}}
    <h2>Step 2: Enter Verification Code</h2>
    
    {{#if error}}
        <div class="error" role="alert" aria-live="polite">{{error}}</div>
    {{/if}}
    
    <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="{{id}}" />
    
        <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.
  6. 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 to index.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-js in 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 options
        • originator: 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. %token is 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 response
        • err: Error occurred (invalid number, API key issue, insufficient balance). Log detailed error and show user-friendly message.
        • response: Request successful. response.id is the unique identifier for this verification attempt.

    Production validation example with libphonenumber-js:

    javascript
    const { 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:

    CodeDescriptionSolution
    2Request not allowedCheck API key permissions
    9Missing or invalid parametersValidate all required fields
    20Verification ID not foundID expired or already verified
    21Invalid recipient numberValidate E.164 format
    23Invalid tokenToken incorrect or expired
    25Too many verification attemptsImplement rate limiting
  7. Step 3: Verification Result (views/step3.handlebars) Show success message upon correct verification:

    handlebars
    {{!-- views/step3.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 }
    );
  8. 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 to index.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 call
      • id: Verification ID from the create call (passed via hidden form field)
      • token: OTP entered by user
      • callback(err, response):
        • err: Token was incorrect, expired, or ID was invalid. Render step2 with error message, pass id back 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.create and messagebird.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.log and console.error. For production, use a dedicated logging library (Winston or Pino) to structure logs, write to files, and set log levels.

Winston configuration example:

javascript
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 .env and never commit to version control. Ensure .env is 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-otp from abuse. Repeated code requests cost money and harass users. Implement rate limiting per IP or user account with express-rate-limit. MessageBird limits 3 verification attempts per ID (maxAttempts: 3), but add application-level rate limiting. Consider stricter limits on /verify-otp to prevent token guessing attacks.

    Install rate limiting:

    bash
    npm install express-rate-limit

    Configure 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:

    javascript
    const 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:
    javascript
    const 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:

    bash
    npm install csurf
    javascript
    const 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:

  1. Start the Application: Ensure .env has the correct MESSAGEBIRD_API_KEY. Run from your terminal:

    bash
    node index.js

    You should see Server listening on http://localhost:3000.

  2. Manual Browser Testing:

    • Open http://localhost:3000 in 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.
  3. Automated Testing: Implement unit and integration tests with Jest:

    bash
    npm install --save-dev jest supertest

    Example test (test/otp.test.js):

    javascript
    const 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:

    javascript
    jest.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' }] });
                    }
                }
            }
        });
    });
  4. 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" \
           --verbose

      Look for HTML response with Step 2 form and hidden id field. Extract the id value.

    • 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" \
           --verbose

      Look 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):

    sql
    CREATE 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 verificationId server-side in session instead of hidden field for better security with logged-in users.

    javascript
    app.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.type to 'tts' in messagebird.verify.create to send OTP via voice call instead of SMS.

    javascript
    const params = {
        originator: 'VerifyApp',
        template: 'Your verification code is %token.',
        type: 'tts', // Voice call instead of SMS
        language: 'en-us',
        voice: 'male'
    };
  • Customization: Explore verify.create parameters: timeout (default 600s = 10 minutes), tokenLength (default 6), language, and voice (for TTS).

  • UI/UX: Improve interface and provide clearer feedback.

  • Resend Code: Allow users to request codes again after a delay.

    javascript
    app.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_KEY in .env is 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. Use libphonenumber-js for 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 timeout duration (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 LogsSMS 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:

  1. ☐ Check MESSAGEBIRD_API_KEY is set correctly in .env
  2. ☐ Verify phone number includes '+' and country code
  3. ☐ Confirm MessageBird account has sufficient balance
  4. ☐ Check MessageBird dashboard logs for delivery status
  5. ☐ Verify originator settings for your target region
  6. ☐ Test with a different phone number
  7. ☐ Check console logs for detailed error messages
  8. ☐ 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 .env files.

    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_key or docker-compose
  • 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.

    bash
    npm 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 (not devDependencies). Run npm install --production in deployment environment.

Docker deployment example:

dockerfile
# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install --production

COPY . .

EXPOSE 3000

CMD ["node", "index.js"]
yaml
# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - MESSAGEBIRD_API_KEY=${MESSAGEBIRD_API_KEY}
      - NODE_ENV=production
    restart: unless-stopped

Frequently 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.