code examples

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

How to Implement MessageBird OTP & 2FA in Next.js: Complete Tutorial

Learn how to build SMS-based OTP verification and two-factor authentication in Next.js with MessageBird Verify API. Includes working code, security best practices, and step-by-step setup guide.

MessageBird Next.js OTP/2FA Integration: Complete Implementation Guide

Learn how to implement SMS-based One-Time Password (OTP) verification and Two-Factor Authentication (2FA) in your Next.js application using MessageBird's Verify API. This step-by-step tutorial shows you how to build a complete OTP authentication flow—from sending verification codes to validating user input—with production-ready code examples.

By following this guide, you'll build a secure Next.js authentication system that protects user accounts with SMS verification, reduces fraudulent signups, and enhances account security with two-factor authentication.

What You'll Build: SMS OTP Authentication System

Goal: Build SMS-based OTP verification for Next.js using MessageBird's Verify API.

What This Solves: Add a second authentication factor to protect user accounts, verify phone numbers during registration, prevent fraudulent signups, and secure sensitive transactions. Common use cases include user registration verification, login 2FA, password reset confirmation, and transaction authorization.

Time Estimate: 30-45 minutes for basic implementation, plus additional time for production hardening.

Technologies Used:

  • Next.js 15.x – React framework with API Routes for server-side logic
  • Node.js v20 LTS or v22 LTS – Runtime environment for backend operations
  • MessageBird Verify API – SMS and voice OTP delivery service
  • MessageBird Node.js SDK – Official npm package (messagebird) for API integration
  • React – Frontend UI components and state management
  • TypeScript – Type-safe development (recommended)
  • Prisma (Optional) – Database ORM for persisting user verification status
  • Tailwind CSS (Optional) – Utility-first styling framework

Authentication Flow Architecture:

<table> <tr> <th><strong>Step</strong></th> <th><strong>Component</strong></th> <th><strong>Action</strong></th> </tr> <tr> <td>1</td> <td>Frontend UI</td> <td>User enters phone number</td> </tr> <tr> <td>2</td> <td>API Route</td> <td>Frontend sends number to <code>/api/auth/request-otp</code></td> </tr> <tr> <td>3</td> <td>MessageBird SDK</td> <td>API Route calls MessageBird Verify API to request OTP</td> </tr> <tr> <td>4</td> <td>MessageBird Service</td> <td>Sends OTP via SMS, returns verification <code>id</code></td> </tr> <tr> <td>5</td> <td>API Route</td> <td>Stores <code>id</code> temporarily (session/cookie/server-side) and signals success</td> </tr> <tr> <td>6</td> <td>Frontend UI</td> <td>Prompts user to enter received OTP code</td> </tr> <tr> <td>7</td> <td>Frontend UI</td> <td>User enters code, sends it with verification <code>id</code> to <code>/api/auth/verify-otp</code></td> </tr> <tr> <td>8</td> <td>MessageBird SDK</td> <td>API Route calls Verify API with <code>id</code> and user's <code>token</code> (OTP)</td> </tr> <tr> <td>9</td> <td>MessageBird Service</td> <td>Validates token against <code>id</code>, responds to API Route</td> </tr> <tr> <td>10</td> <td>API Route & Database</td> <td>Processes response, updates user profile if successful (e.g., <code>isTwoFactorEnabled = true</code>)</td> </tr> </table>

(Note: An architecture diagram image (e.g., PNG/SVG) would improve visualization across platforms.)

Prerequisites:

  • Node.js (v20 LTS or v22 LTS recommended as of 2025) and npm/yarn installed
  • A MessageBird account with a Live API Key (Sign up at messagebird.com)
  • Basic understanding of React and Next.js
  • (Optional) PostgreSQL database and connection string if using Prisma
  • (Optional) Familiarity with Tailwind CSS for styling

Step 1: Project Setup and Installation

Initialize your Next.js project and install the MessageBird SDK to get started with OTP authentication.

  1. Create Next.js App

    Open your terminal and run the following command, choosing options like TypeScript (recommended), Tailwind CSS (optional), and App Router (recommended):

    bash
    npx create-next-app@latest messagebird-otp-nextjs
    cd messagebird-otp-nextjs

    Follow the prompts. This guide assumes you're using the App Router and TypeScript.

  2. Install Dependencies

    Install the MessageBird Node.js SDK:

    bash
    npm install messagebird

    (Optional) If you plan to store user data or 2FA status, install Prisma:

    bash
    npm install prisma @prisma/client
    npm install -D prisma
    npx prisma init --datasource-provider postgresql # Or your preferred DB
  3. Configure Environment Variables

    Create a .env.local file in the root of your project. Never commit this file to version control. Add your MessageBird Live API Key and (if using Prisma) your database connection string.

    ini
    # .env.local
    
    # MessageBird API Key (Get from MessageBird Dashboard > Developers > API access)
    MESSAGEBIRD_API_KEY="YOUR_LIVE_API_KEY_HERE"
    
    # (Optional) Database URL for Prisma
    # Example for PostgreSQL: postgresql://USER:PASSWORD@HOST:PORT/DATABASE
    DATABASE_URL="YOUR_DATABASE_CONNECTION_STRING"
    • MESSAGEBIRD_API_KEY: Obtain this from your MessageBird Dashboard under Developers > API access (REST). Use a Live key for actual SMS sending. Test keys won't send real messages.
    • DATABASE_URL: Your database connection string. Format depends on your database provider.
  4. (Optional) Prisma Schema Setup

    If using Prisma, modify the generated prisma/schema.prisma file to include relevant user fields for 2FA.

    prisma
    // prisma/schema.prisma
    
    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "postgresql" // Or your chosen provider
      url      = env("DATABASE_URL")
    }
    
    model User {
      id                 String    @id @default(cuid())
      email              String?   @unique // Example field
      phoneNumber        String?   @unique // Store verified phone number
      isTwoFactorEnabled Boolean   @default(false) // Flag for 2FA status
      createdAt          DateTime  @default(now())
      updatedAt          DateTime  @updatedAt
    
      // Add other user fields as needed
    }

    Apply the schema to your database:

    bash
    npx prisma db push
    # You might also need: npx prisma generate

    This creates the User table in your database.

Step 2: Build OTP API Routes with MessageBird Verify API

Create two Next.js API routes: one to generate and send OTP codes via MessageBird, and another to verify user input against the generated code.

  1. Create Helper for MessageBird Client

    Initialize the MessageBird client once for better performance. Create lib/messagebird.ts:

    typescript
    // lib/messagebird.ts
    import messagebird from 'messagebird';
    
    const apiKey = process.env.MESSAGEBIRD_API_KEY;
    
    if (!apiKey) {
      throw new Error('MESSAGEBIRD_API_KEY environment variable is not set.');
    }
    
    // Initialize with your API key
    export const mbClient = messagebird(apiKey);
  2. API Route to Request OTP (/api/auth/request-otp)

    Create the file app/api/auth/request-otp/route.ts:

    typescript
    // app/api/auth/request-otp/route.ts
    import { NextRequest, NextResponse } from 'next/server';
    import { mbClient } from '@/lib/messagebird'; // Adjust path if needed
    
    export async function POST(req: NextRequest) {
      try {
        const body = await req.json();
        const { phoneNumber } = body;
    
        // Basic validation (E.164 format). Consider using a library like
        // 'google-libphonenumber' for more robust validation in production.
        if (!phoneNumber || typeof phoneNumber !== 'string' || !/^\+[1-9]\d{1,14}$/.test(phoneNumber)) {
          return NextResponse.json({ error: 'Invalid phone number format. Use E.164 format (e.g., +1234567890).' }, { status: 400 });
        }
    
        // Parameters for the MessageBird Verify API
        const params = {
          // Sender ID: Alphanumeric (max 11 chars) or phone number. Note: Alphanumeric IDs
          // are not supported in all countries (e.g., US/Canada). Using a purchased virtual
          // number from MessageBird is often the most reliable method globally for a custom ID.
          originator: 'VerifyApp',
          type: 'sms', // Use 'tts' for voice call OTP
          template: 'Your verification code is %token.', // Message template
          // tokenLength: 6, // Default is 6
          // timeout: 60, // Default is 30 seconds
        };
    
        // Note: This uses a Promise wrapper for the callback-based SDK method.
        // Check if the current 'messagebird' SDK version supports native Promises
        // (e.g., `await mbClient.verify.create(...)`) for potentially cleaner async/await syntax.
        return new Promise((resolve) => {
          mbClient.verify.create(phoneNumber, params, (err: any, response: any) => {
            if (err) {
              console.error("MessageBird Verify Create Error:", err);
              // Provide more specific error messages based on err.errors
              let errorMessage = 'Failed to send verification code.';
              if (err.errors && err.errors[0]) {
                errorMessage = err.errors[0].description || errorMessage;
                // Handle specific errors like invalid number format explicitly if needed
                if (err.errors[0].code === 21) { // Example: Code for invalid parameter
                  errorMessage = 'Invalid phone number provided to MessageBird.';
                }
              }
              resolve(NextResponse.json({ error: errorMessage }, { status: 500 }));
            } else {
              console.log("MessageBird Verify Create Response:", response);
              // IMPORTANT: Securely handle the response.id
              // For this example, we return it. In production, consider:
              // 1. Storing it in a secure, httpOnly cookie associated with the user's session.
              // 2. Storing it server-side (e.g., Redis, database) linked to the session ID.
              // Returning it directly to the client is simpler but less secure if not handled carefully frontend.
              resolve(NextResponse.json({ success: true, verifyId: response.id }, { status: 200 }));
            }
          });
        });
    
      } catch (error) {
        console.error("Request OTP Error:", error);
        return NextResponse.json({ error: 'An unexpected error occurred.' }, { status: 500 });
      }
    }
    • Validation: Includes basic E.164 format check. For production, use google-libphonenumber for robust validation.
    • mbClient.verify.create: Calls the MessageBird API.
      • phoneNumber: The user's number in international E.164 format (e.g., +14155552671).
      • params: Configuration options.
        • originator: Alphanumeric IDs vary by country (not supported in US/Canada). Use a purchased virtual number from MessageBird for global reliability.
        • template: The message text. %token is replaced by the generated OTP.
        • timeout: How long the OTP remains valid (default 30s).
    • Error Handling: Logs errors and returns user-friendly messages. Parses MessageBird-specific errors if available.
    • Success Response: Returns success: true and the verifyId. Handle this verifyId securely – see comments in code.
    • Promise Wrapper Note: Check if your SDK version supports native async/await for cleaner syntax.
  3. API Route to Verify OTP (/api/auth/verify-otp)

    Create the file app/api/auth/verify-otp/route.ts:

    typescript
    // app/api/auth/verify-otp/route.ts
    import { NextRequest, NextResponse } from 'next/server';
    import { mbClient } from '@/lib/messagebird'; // Adjust path
    // import { prisma } from '@/lib/prisma'; // Uncomment if using Prisma
    
    export async function POST(req: NextRequest) {
      try {
        const body = await req.json();
        const { verifyId, token } = body;
    
        // Basic validation
        if (!verifyId || typeof verifyId !== 'string' || verifyId.trim() === '') {
          return NextResponse.json({ error: 'Verification ID is missing.' }, { status: 400 });
        }
        if (!token || typeof token !== 'string' || !/^\d{6}$/.test(token)) { // Assuming 6-digit token
          return NextResponse.json({ error: 'Invalid OTP format. Must be 6 digits.' }, { status: 400 });
        }
    
        // Note: This uses a Promise wrapper for the callback-based SDK method.
        // Check if the current 'messagebird' SDK version supports native Promises
        // (e.g., `await mbClient.verify.verify(...)`) for potentially cleaner async/await syntax.
        return new Promise((resolve) => {
          mbClient.verify.verify(verifyId, token, (err: any, response: any) => {
            if (err) {
              console.error("MessageBird Verify Verify Error:", err);
              let errorMessage = 'Failed to verify code.';
              if (err.errors && err.errors[0]) {
                errorMessage = err.errors[0].description || errorMessage;
                 // Example: Handle specific error descriptions like expired/invalid ID
                if (err.errors[0].code === 10) { // Example code often associated with 'not_found' (expired/invalid ID)
                     errorMessage = 'Verification ID is invalid or has expired.';
                }
                // Check err.errors[0].description for more specific details from MessageBird
              }
              // Common error: Token is incorrect or expired.
              resolve(NextResponse.json({ error: errorMessage, verified: false }, { status: 400 }));
            } else {
              // Verification successful!
              console.log("MessageBird Verify Verify Response:", response);
    
              // Optional: Update user record in database if verification is for enabling 2FA
              // const userId = ''; // Get userId from session/token
              // try {
              //   await prisma.user.update({
              //     where: { id: userId },
              //     data: { isTwoFactorEnabled: true, phoneNumber: response.recipient }, // Store verified number
              //   });
              // } catch (dbError) {
              //    console.error("Database update error:", dbError);
              //    // Decide how to handle DB error – maybe verification still counts?
              //    resolve(NextResponse.json({ error: 'Verification successful, but failed to update profile.' }, { status: 500 }));
              //    return;
              // }
    
              // Clear the verifyId from session/cookie here if stored server-side
    
              resolve(NextResponse.json({ success: true, verified: true }, { status: 200 }));
            }
          });
        });
    
      } catch (error) {
        console.error("Verify OTP Error:", error);
        return NextResponse.json({ error: 'An unexpected error occurred.', verified: false }, { status: 500 });
      }
    }
    • Validation: Checks for verifyId and basic OTP format (assuming 6 digits).
    • mbClient.verify.verify: Calls MessageBird to check the code.
      • verifyId: The ID received from the create call.
      • token: The 6-digit code entered by the user.
    • Error Handling: Handles incorrect token, expired token/ID, or other API errors. Check err.errors[0].description for specifics.
    • Success Response: Returns verified: true.
    • (Optional) Database Update: Includes commented-out code showing how to update a user record in Prisma upon successful verification (e.g., enabling 2FA or storing the verified phone number). Get the userId from the user's session or authentication token.
    • Promise Wrapper Note: Check if your SDK version supports native async/await for cleaner syntax.

Step 3: Create the Frontend OTP Verification UI

Build React components to create a user-friendly OTP input interface with proper error handling and loading states.

  1. Create the Page

    Create a page file, for example, app/verify/page.tsx:

    typescript
    // app/verify/page.tsx
    'use client'; // This component interacts with user state and browser APIs
    
    import React, { useState } from 'react';
    
    export default function VerifyPage() {
      const [phoneNumber, setPhoneNumber] = useState('');
      const [otpCode, setOtpCode] = useState('');
      // SECURITY WARNING: Storing verifyId in client-side state is simple for demos but NOT recommended for production.
      // See Security Considerations section for better approaches (httpOnly cookies, server sessions).
      const [verifyId, setVerifyId] = useState<string | null>(null);
      const [isLoading, setIsLoading] = useState(false);
      const [error, setError] = useState<string | null>(null);
      const [message, setMessage] = useState<string | null>(null);
      const [isOtpSent, setIsOtpSent] = useState(false);
      const [isVerified, setIsVerified] = useState(false);
    
      const handleRequestOtp = async (e: React.FormEvent) => {
        e.preventDefault();
        setIsLoading(true);
        setError(null);
        setMessage(null);
        setIsOtpSent(false);
        setVerifyId(null); // Clear previous ID
    
        try {
          const res = await fetch('/api/auth/request-otp', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ phoneNumber }),
          });
    
          const data = await res.json();
    
          if (!res.ok) {
            throw new Error(data.error || 'Failed to request OTP');
          }
    
          setVerifyId(data.verifyId); // Store the ID from the backend (see security warning above)
          setIsOtpSent(true);
          setMessage('OTP sent successfully! Check your phone.');
    
        } catch (err: any) {
          setError(err.message || 'An error occurred.');
        } finally {
          setIsLoading(false);
        }
      };
    
      const handleVerifyOtp = async (e: React.FormEvent) => {
        e.preventDefault();
        setIsLoading(true);
        setError(null);
        setMessage(null);
    
        if (!verifyId) {
            setError("Verification process not initiated correctly. Request OTP again.");
            setIsLoading(false);
            return;
        }
    
        try {
          const res = await fetch('/api/auth/verify-otp', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            // Send the stored verifyId and the user's input code
            body: JSON.stringify({ verifyId: verifyId, token: otpCode }),
          });
    
          const data = await res.json();
    
          if (!res.ok || !data.verified) {
            throw new Error(data.error || 'Failed to verify OTP');
          }
    
          setIsVerified(true);
          setMessage('Phone number verified successfully!');
          // Reset form state after success
          setIsOtpSent(false);
          setVerifyId(null);
          setOtpCode('');
          // Optionally redirect or update UI further
    
        } catch (err: any) {
          setError(err.message || 'An error occurred during verification.');
           // Keep OTP form visible on verification failure to allow retry
        } finally {
          setIsLoading(false);
        }
      };
    
      // Basic styling with Tailwind CSS (optional)
      const inputStyle = "border p-2 rounded w-full mb-4";
      const buttonStyle = "bg-blue-500 text-white p-2 rounded hover:bg-blue-600 disabled:opacity-50";
    
      return (
        <div className="max-w-md mx-auto mt-10 p-6 border rounded shadow">
          <h1 className="text-2xl font-bold mb-6 text-center">Verify Phone Number</h1>
    
          {error && <p className="text-red-500 mb-4 bg-red-100 p-3 rounded">{error}</p>}
          {message && <p className="text-green-500 mb-4 bg-green-100 p-3 rounded">{message}</p>}
    
          {!isOtpSent && !isVerified && (
            <form onSubmit={handleRequestOtp}>
              <label htmlFor="phoneNumber" className="block mb-2 font-medium">Phone Number (E.164 format)</label>
              <input
                type="tel"
                id="phoneNumber"
                value={phoneNumber}
                onChange={(e) => setPhoneNumber(e.target.value)}
                placeholder="+1234567890"
                required
                className={inputStyle}
                disabled={isLoading}
              />
              <button type="submit" disabled={isLoading || !phoneNumber} className={`${buttonStyle} w-full`}>
                {isLoading ? 'Sending…' : 'Send OTP'}
              </button>
            </form>
          )}
    
          {isOtpSent && !isVerified && (
            <form onSubmit={handleVerifyOtp}>
               <p className="mb-4">Enter the 6-digit code sent to {phoneNumber}:</p>
               <label htmlFor="otpCode" className="block mb-2 font-medium">OTP Code</label>
               <input
                 type="text"
                 id="otpCode"
                 value={otpCode}
                 onChange={(e) => setOtpCode(e.target.value)}
                 placeholder="123456"
                 required
                 maxLength={6} // Assuming 6 digits
                 pattern="\d{6}" // Basic pattern validation
                 className={`${inputStyle} tracking-widest text-center`} // Style for OTP input
                 disabled={isLoading}
               />
               <button type="submit" disabled={isLoading || otpCode.length !== 6} className={`${buttonStyle} w-full`}>
                 {isLoading ? 'Verifying…' : 'Verify Code'}
               </button>
               <button
                 type="button"
                 onClick={() => { setIsOtpSent(false); setError(null); setMessage(null); setOtpCode(''); setVerifyId(null); }}
                 className="text-sm text-blue-600 hover:underline mt-4 text-center block w-full disabled:opacity-50"
                 disabled={isLoading}
               >
                 Entered wrong number? Start over.
               </button>
            </form>
          )}
    
          {isVerified && (
              <div className="text-center">
                  <p className="text-xl font-semibold text-green-700">Verification Complete!</p>
                   <button
                     type="button"
                     onClick={() => { setIsVerified(false); setPhoneNumber(''); /* Reset more state if needed */ }}
                     className={`${buttonStyle} mt-6`}
                   >
                     Verify Another Number
                   </button>
              </div>
          )}
        </div>
      );
    }
    • 'use client': Required because this component uses useState and interacts with browser events/APIs.
    • State Management: Uses useState to manage phone number input, OTP code input, loading state, errors, success messages, the verifyId, and the UI flow (isOtpSent, isVerified).
    • handleRequestOtp: Sends the phone number to /api/auth/request-otp. On success, stores the returned verifyId in state and updates the UI to show the OTP input form.
    • handleVerifyOtp: Sends the verifyId (retrieved from state) and the user-entered otpCode to /api/auth/verify-otp. Updates UI based on success or failure.
    • UI Flow: Conditionally renders the phone number form, the OTP form, or the success message based on isOtpSent and isVerified states.
    • Error/Message Display: Shows feedback to the user.
    • Security Note: Storing verifyId in React state is acceptable for simplified examples but not recommended for production. This approach is vulnerable to issues like losing state on page refresh and potential client-side manipulation. A more robust approach involves managing the verifyId server-side, typically by storing it in a secure, httpOnly cookie tied to the user's session or in a server-side store (like Redis or a database) linked to a session identifier. The server would then retrieve the verifyId based on the session when the verification request arrives, rather than relying on the client to send it back.

Security Best Practices for OTP Authentication

  • API Key Security: Your MESSAGEBIRD_API_KEY is highly sensitive. Keep it in .env.local and ensure this file is listed in your .gitignore. Never expose it client-side. Use environment variables in your deployment environment.
  • Rate Limiting: Implement rate limiting on your API routes (/api/auth/request-otp, /api/auth/verify-otp) to prevent abuse (e.g., flooding users with OTPs, brute-forcing codes). Use middleware in Next.js or libraries like rate-limiter-flexible, or leverage platform features like Vercel's built-in IP rate limiting.
  • Input Validation: Rigorously validate all inputs on the server-side (API routes). Check phone number format (E.164), token format (digits, length), and verifyId presence/format.
  • verifyId Handling (Crucial): Returning verifyId directly to the client and storing it in frontend state is convenient for demos but insecure for production. An attacker could potentially intercept or manipulate it. Prefer server-side management: Store the verifyId in a secure, httpOnly cookie (potentially encrypted) or in server-side session storage (e.g., Redis linked to a session ID stored in a secure cookie). When verifying, retrieve the verifyId on the server based on the user's session, not from the client request body.
  • Protecting API Routes: Ensure only authorized users can initiate the 2FA setup or verification process if it's tied to user accounts. Implement authentication checks in your API routes before calling MessageBird.
  • Token Timeout: MessageBird tokens remain valid for 30 seconds by default. Inform users and handle expired tokens gracefully.

Error Handling Best Practices

  • API Route Errors: Use try...catch blocks in API routes. Log detailed errors server-side (including MessageBird error objects) for debugging. Return clear, user-friendly error messages to the frontend, avoiding exposing sensitive details.
  • MessageBird Errors: Parse the err.errors array provided by the MessageBird SDK callback for specific error codes and descriptions (e.g., invalid phone number, invalid token, expired token). Check the description field for the most accurate information. See examples in API route code. Refer to the official MessageBird API documentation for a list of error codes.
  • Frontend Errors: Display errors received from the API to the user clearly. Handle network errors during fetch calls.
  • Logging: Implement structured logging (e.g., using Pino or Winston) on the server-side to capture request details, successes, and errors for monitoring and troubleshooting.

How to Test Your OTP Implementation

  • Manual Testing:
    1. Run the app (npm run dev).
    2. Navigate to /verify.
    3. Enter your real phone number in E.164 format. Click "Send OTP".
    4. Check your phone for the SMS.
    5. Enter the received code. Click "Verify Code".
    6. Test error cases: invalid phone number format, incorrect OTP, expired OTP (wait > 30s), requesting OTP multiple times quickly (test rate limiting if implemented).
  • Unit Testing (API Routes): Use a testing framework like Jest. Mock the messagebird SDK to simulate successful and error responses from verify.create and verify.verify without making actual API calls. Test input validation logic.
  • Integration Testing (Optional): Use tools like Playwright or Cypress to automate browser interactions, simulating the full user flow from entering the phone number to verifying the OTP. This requires careful setup, potentially involving test MessageBird credentials or mocking the API at the network level.

Deploying Your Next.js OTP Application

  • Platform: Deploy your Next.js application to platforms like Vercel, Netlify, AWS Amplify, or a traditional Node.js server.
  • Environment Variables: Configure MESSAGEBIRD_API_KEY and DATABASE_URL (if used) as environment variables in your deployment platform's settings. Never hardcode them.
  • Build Command: Typically npm run build.
  • Database: Ensure your deployed application can connect to your production database. Configure connection pooling appropriately.
  • HTTPS: Always use HTTPS in production. Deployment platforms usually handle this automatically.

Common Issues and Troubleshooting

  • Invalid API Key: Error messages often indicate authentication failure. Double-check your MESSAGEBIRD_API_KEY in .env.local and your deployment environment variables. Ensure it's a Live key.
  • Invalid Phone Number: MessageBird expects E.164 format (+ followed by country code and number, e.g., +14155552671). Ensure your frontend sends this format and your backend validation checks for it. Check the err.errors[0].description from MessageBird for specifics (error code 21 often relates to invalid parameters).
  • Alphanumeric Sender ID Not Working: Support for alphanumeric originator values varies by country (e.g., not supported in the US/Canada). Test with a purchased virtual number from MessageBird as the originator for better reliability, or stick to the default 'Code' / 'MessageBird' if acceptable.
  • Token Expired / Invalid: The default timeout is 30 seconds. If the user takes longer, verify.verify will fail. Handle this error by checking the err.errors[0].description (which might indicate an invalid or expired token/ID, sometimes associated with code 10) and prompt the user to request a new code. Ensure you're using the correct verifyId (especially relevant if using the less secure client-side storage method). Refer to official MessageBird error documentation for definitive code meanings.
  • Rate Limits: MessageBird enforces rate limits. If you send too many requests too quickly, you'll receive errors. Implement client-side and server-side rate limiting (see Security Considerations).
  • Message Not Received: Check MessageBird dashboard logs for delivery status. Ensure the number is correct and the phone has service. Test with different carriers if possible. Temporary carrier issues can occur.
  • verifyId Handling: If you handle the verifyId insecurely on the client-side, it can be lost (breaking the flow) or potentially compromised. Server-side session storage is strongly recommended for production environments.
  • Cost: Sending SMS messages incurs costs. Monitor your MessageBird usage.

Complete Code Repository

Coming Soon: A complete working example repository with all the code from this tutorial will be available for reference. This will include the full Next.js project structure, environment configuration examples, and additional security enhancements.

Next Steps: Enhancing Your OTP System

You've successfully built SMS-based OTP verification in Next.js using MessageBird's Verify API. Your implementation includes API routes for sending and verifying codes, a complete frontend UI, and essential security considerations.

Production Checklist:

  • Implement server-side session storage for verifyId instead of client-side state
  • Add rate limiting to prevent abuse and SMS pumping attacks
  • Set up monitoring for failed verification attempts
  • Configure database integration to persist verification status
  • Test with real phone numbers across different carriers and countries

Enhancement Ideas:

  • Add voice OTP support (type: 'tts') as a fallback for users who don't receive SMS
  • Implement retry logic with exponential backoff
  • Create admin dashboard to monitor verification metrics
  • Add support for multiple authentication methods (email OTP, authenticator apps)

Additional Resources:

Frequently Asked Questions

How to set up MessageBird OTP in Next.js?

Start by creating a new Next.js project and installing the required dependencies, including the MessageBird Node.js SDK and optionally Prisma for database interactions. Set up environment variables for your MessageBird API key and database URL in a .env.local file. If using Prisma, modify the schema.prisma file to store user data related to 2FA.

What is the MessageBird Verify API?

The MessageBird Verify API is a service that allows you to send and verify one-time passwords (OTPs) via SMS or voice calls. It's used to implement two-factor authentication (2FA) or verify phone numbers, enhancing security and reducing fraudulent sign-ups.

Why use OTP for user authentication?

OTP adds an extra layer of security by requiring a code sent to the user's phone, making it much harder for unauthorized access even if passwords are compromised. This helps protect against account takeovers and strengthens overall security.

When should I implement 2FA using MessageBird?

Implement 2FA whenever enhanced security is needed, like during login, high-value transactions, or account changes. This is especially important for sensitive data or when regulatory compliance requires stronger authentication methods.

Can I customize the OTP message template?

Yes, the MessageBird Verify API allows message customization. You can set the 'template' parameter in the API request. Use '%token' as a placeholder within the template. For example, 'Your verification code is %token.'

How to handle MessageBird API errors in Next.js?

Use try...catch blocks in your API routes to handle errors during OTP requests and verification. Log the detailed error object from the MessageBird SDK for debugging purposes. Return clear and user-friendly error messages to the frontend.

What is the recommended way to store the verification ID?

For production, store the 'verifyId' securely on the server-side. Options include using secure, HTTPOnly cookies (potentially encrypted) linked to the user's session or storing it in server-side session storage like Redis, linked to a session identifier in a secure cookie.

How to verify the OTP code entered by the user?

The frontend sends the 'verifyId' and the user-entered OTP code to the /api/auth/verify-otp route. This route uses the MessageBird SDK to call the verify.verify API method, which checks if the code is valid and responds accordingly. Ensure 'verifyId' is handled securely.

What are the security best practices for MessageBird OTP integration?

Key security measures include protecting your MessageBird API key, implementing rate limiting, rigorous input validation, secure verifyId handling, and protecting API routes with authentication. Ensure only authorized users can initiate or verify 2FA.

How to test the MessageBird OTP implementation effectively?

Testing methods include manual testing of the complete user flow, unit testing API routes with mocked MessageBird responses, and optionally automated integration testing using tools like Playwright or Cypress to simulate browser interactions.

What to do if the user doesn't receive the SMS message?

Check the MessageBird dashboard logs for delivery status. Verify the phone number's correctness and the user's network connectivity. Consider potential temporary carrier issues and test with different carriers if necessary.

What is the E.164 phone number format?

E.164 is an international standard for phone number formatting. It begins with a '+' sign followed by the country code and then the national subscriber number. Example: +1234567890.

Where can I find the complete code example for the MessageBird OTP tutorial?

The tutorial mentions a complete code repository. If not found within the tutorial, contact the tutorial provider or search for relevant repositories online (e.g., on GitHub).

How to handle rate limiting for MessageBird API calls?

Implement rate limiting in your API routes to prevent abuse. This can be done with middleware in Next.js using libraries like 'rate-limiter-flexible', or through platform features (Vercel's IP rate limiting).

What are the common troubleshooting steps for MessageBird OTP issues?

Common issues include incorrect or expired tokens, invalid phone numbers, and API key errors. Make sure your MessageBird API key is valid and stored correctly. Also verify phone number format. Check MessageBird error logs for detailed information and potential carrier-side issues.