code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / nextjs

How to Implement Infobip SMS OTP & 2FA in Next.js: Complete Node.js Guide

Step-by-step tutorial for implementing SMS OTP verification with Infobip API in Next.js. Includes code examples, security best practices, and production deployment patterns.

Developer Guide: Implementing Infobip OTP/2FA in Next.js with Node.js

Learn how to build secure SMS-based two-factor authentication (2FA) in Next.js applications using Infobip's API. This comprehensive tutorial covers implementing OTP verification for user registration, login security, and transaction confirmation with complete code examples and production-ready patterns.

⚠️ Critical Security Notice:

Before implementing SMS-based 2FA, understand these security limitations:

  • NIST Restriction: NIST SP 800-63B designates SMS/PSTN authentication as "RESTRICTED" due to known vulnerabilities including SIM swapping and SS7 protocol attacks
  • OWASP Guidance: SMS OTP "should not be used to protect applications that hold Personally Identifiable Information (PII) or where there is financial risk"
  • Regulatory Requirement: If using SMS 2FA, you must offer at least one non-RESTRICTED alternative (authenticator apps, hardware tokens, or passkeys)
  • Use Case Suitability: SMS OTP is acceptable for low-risk scenarios like basic account verification but unsuitable for financial transactions, healthcare data access, or sensitive PII protection

Recommended Alternatives: For higher security requirements, consider implementing TOTP (Time-based One-Time Password) apps, WebAuthn/FIDO2, or hardware security keys alongside or instead of SMS.

What You'll Build: A complete SMS OTP authentication system that sends verification codes via Infobip's messaging service and validates user input through secure API routes.

Real-World Use Cases:

  • Financial Services: Banks use SMS OTP as part of multi-layered authentication for transaction confirmations and secure login flows. However, per NIST guidelines, SMS must be combined with non-RESTRICTED authenticators (hardware tokens, biometrics) for PCI-DSS compliance and financial transaction protection.
  • E-commerce Platforms: Online retailers implement phone verification during checkout to prevent fraud and chargebacks – an appropriate low-to-medium risk use case for SMS OTP.
  • Healthcare Applications: Medical platforms verifying patient identity before allowing access to sensitive health records must implement SMS OTP alongside stronger authentication methods to meet HIPAA security requirements, as SMS alone is insufficient for PHI (Protected Health Information) access.
  • Enterprise Systems: Companies secure employee access to internal systems and validate identity for password resets – suitable for SMS OTP when combined with other security controls for high-privilege accounts.

Business Benefits:

  • Fraud Reduction: Studies show 2FA reduces account takeover attacks by up to 99.9%
  • Regulatory Compliance: Meets requirements for PCI-DSS, HIPAA, and other security frameworks
  • Customer Trust: Users report higher confidence in platforms that implement additional security layers
  • Reduced Support Costs: Automated phone verification reduces manual identity verification workload

Master the complete implementation process including Infobip API configuration, secure Next.js API route development, React component creation, and production deployment best practices. Perfect for developers building authentication systems, e-commerce checkout flows, or any application requiring phone number verification.

For more Next.js authentication patterns, check out our guide on implementing JWT authentication in Next.js and building secure user registration systems.

Building SMS Authentication: Project Overview

What We're Building:

We will implement a feature where a user can enter their phone number, receive an OTP via SMS powered by Infobip, and then verify that OTP within the Next.js application. This forms the core of a 2FA or phone verification system.

Problem Solved:

Compliance and Regulatory Requirements:

  • GDPR Compliance: Phone numbers constitute personal data requiring valid legal basis (consent or legitimate interest). For transactional OTP messages, these are considered necessary service communications and don't require advertising consent. However, you must implement proper data retention policies and honor data subject rights including data portability and erasure requests.

  • PCI-DSS Requirements: Payment processing systems often mandate multi-factor authentication for sensitive operations. SMS OTP satisfies PCI-DSS requirements for "something you have" authentication factors.

  • Data Retention: Under GDPR, organizations must define retention periods for personal data and automate deletion when retention periods expire or upon data subject request.

  • Enhanced Security: Adds a second layer of authentication beyond passwords, with ENISA recommending 2FA implementation for high-risk cases involving personal data processing.

  • User Verification: Confirms user identity by validating phone number ownership.

  • Fraud Prevention: Helps prevent fake account creation and secures transactions. Financial institutions report up to 99.9% reduction in account takeover attacks when implementing proper 2FA systems.

Technologies Involved:

  • Next.js: React framework for building the frontend UI and backend API routes.
  • Node.js: Runtime environment for Next.js API routes.
  • Infobip: Cloud communications platform providing the 2FA/OTP service via API.
  • Infobip Node.js SDK: Simplifies interaction with the Infobip API.
  • React: For building the frontend components within Next.js.

System Architecture:

The flow involves three main components:

  1. Frontend (Next.js/React): Captures the user's phone number and the OTP they receive. Makes requests to the Next.js backend.
  2. Backend (Next.js API Routes): Acts as a secure intermediary. It receives requests from the frontend, interacts with the Infobip API using credentials stored securely, and sends responses back to the frontend.
  3. Infobip API: Handles the generation, sending (via SMS), and verification of the OTP.
text
[User's Browser: Frontend UI] <---> [Next.js Server: API Routes] <---> [Infobip API]
  (Enter Phone #) -----------> /api/send-otp ------------> (Send OTP Request)
                                                        <------------ (pinId)
                    <----------- (pinId received)
  (Receive SMS, Enter OTP) ---> /api/verify-otp ---------> (Verify OTP Request)
                               (with pinId & OTP)       <------------ (Verification Result)
                    <----------- (Success/Failure)

Prerequisites:

  • Node.js 22 LTS (recommended for 2025) or minimum Node.js 18.17.0 (required for Next.js compatibility). Node.js 22 is the current Active LTS version, providing optimal security and performance until April 2027.
  • npm/yarn: Latest stable version for package management
  • An active Infobip account (Sign up here if you don't have one)
  • Basic understanding of Next.js (Pages Router or App Router), React, and API concepts
  • A text editor (VS Code recommended with extensions for Next.js development)

For additional Next.js setup guidance, see our Next.js development environment setup guide and React authentication patterns tutorial.

Security Prerequisites:

  • HTTPS Environment: Production deployment must use HTTPS to encrypt data in transit. Platforms like Vercel handle this automatically.
  • Secure Environment Management: Understanding of environment variable security and .gitignore configuration
  • Basic Security Awareness: Familiarity with API key protection, rate limiting concepts, and secure coding practices

1. Next.js Project Setup for SMS Authentication

Follow these step-by-step instructions to set up your Next.js project with all required dependencies for SMS authentication.

1.1 Create Next.js Project:

Open your terminal and run:

bash
npx create-next-app@latest infobip-otp-nextjs
# Follow the prompts (e.g., choose TypeScript if desired, App Router or Pages Router)
cd infobip-otp-nextjs

1.2 Install Infobip Node.js SDK:

This SDK provides convenient methods for interacting with the Infobip API.

bash
npm install infobip-api-nodejs-sdk
# or
yarn add infobip-api-nodejs-sdk

1.3 Environment Variables:

We need to store sensitive Infobip credentials securely. Create a file named .env.local in the root of your project. Never commit this file to version control.

Essential .gitignore Setup:

First, ensure your .gitignore file includes all environment files:

gitignore
# Environment variables - never commit these!
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

Create Environment Template:

For team collaboration, create .env.example with the structure but no sensitive values:

env
# .env.example - Template for team setup
INFOBIP_BASE_URL=your_infobip_base_url_here
INFOBIP_API_KEY=your_infobip_api_key_here
INFOBIP_2FA_APP_ID=your_2fa_application_id_here
INFOBIP_2FA_MESSAGE_ID=your_2fa_message_template_id_here
env
# .env.local

# --- IMPORTANT ---
# The values below are placeholders. You MUST replace them with your actual
# credentials and configuration IDs obtained from your Infobip account.
# Failure to do so will prevent the integration from working.
# --- IMPORTANT ---

# Obtain from Infobip Portal (Homepage -> API Base URL for your region)
INFOBIP_BASE_URL=your_infobip_base_url

# Obtain from Infobip Portal (API Key Management)
INFOBIP_API_KEY=your_infobip_api_key

# These will be obtained after configuring the 2FA Application and Message Template in Infobip
INFOBIP_2FA_APP_ID=your_2fa_application_id
INFOBIP_2FA_MESSAGE_ID=your_2fa_message_template_id
  • Why .env.local? Next.js automatically loads variables from this file into process.env on the server side (API routes), keeping your secrets out of the frontend bundle and source control.

1.4 Project Structure:

Your basic structure (using Pages Router for simplicity in examples, adapt if using App Router):

text
infobip-otp-nextjs/
├── lib/
│   └── infobip.js          # Utility for Infobip client initialization
├── pages/
│   ├── api/
│   │   ├── send-otp.js       # API route to send OTP
│   │   └── verify-otp.js     # API route to verify OTP
│   ├── _app.js
│   └── index.js            # Frontend page for OTP interaction
├── public/
├── styles/
├── .env.local              # Your secret credentials
├── package.json
└── node_modules/

2. Infobip SMS API Configuration Guide

Before writing code, we need to configure the necessary components within the Infobip platform.

2.1 Obtain API Key and Base URL:

  1. Log in to your Infobip Portal.

  2. Base URL: On the homepage, locate your API Base URL. It's region-specific (e.g., xxxxx.api.infobip.com).

    Region Selection Guidance:

    • Choose the region closest to your primary user base for optimal latency
    • US East (Virginia) for North American users
    • EU (Ireland) for European users
    • Asia Pacific (Singapore) for Asian users
    • Consider regulatory requirements (EU data residency for GDPR compliance)

    Copy this value into INFOBIP_BASE_URL in your .env.local.

  3. API Key:

    • Navigate to API Key Management. Note: The exact name and location of this section within the Infobip Portal UI might change over time. Look for settings related to API access or developer tools.
    • Click Create API Key.
    • Give it a descriptive name (e.g., Nextjs OTP App - Production).
    • Assign Roles/Permissions:
      • Minimum Required Permission: Grant only the "TFA" scope for 2FA SMS PIN functionality. This is the specific permission required for send and verify PIN operations in 2025.
      • Avoid Broad Permissions: Do not grant full access or additional scopes unless absolutely required for your use case.
    • Security Enhancements:
      • Add IP restrictions for production environments
      • Set expiration dates for API keys where possible
      • Use separate API keys for development, staging, and production environments
    • Click Submit. Copy the generated API key immediately, as it won't be shown again. Paste it into INFOBIP_API_KEY in your .env.local.

    API Key Rotation Best Practices:

    • Rotate API keys every 90-180 days or after team member changes
    • Implement key rotation without service interruption using overlapping key validity periods
    • Monitor API key usage through Infobip's analytics dashboard
    • Immediately revoke compromised keys and generate new ones

2.2 Create a 2FA Application:

This defines the rules for your OTP flow (e.g., how many attempts are allowed, how long the OTP is valid).

  1. In the Infobip Portal, navigate to the Apps section (or search for 2FA Applications).
  2. Click Create Application.
  3. Configure the settings with detailed explanations:
    • Application Name: Give it a clear name (e.g., My Next.js App Verification).
    • Configuration Settings Explained:
      • pinAttempts: Max number of verification attempts (recommended: 3-5). Impact: Higher values increase user convenience but reduce security. Lower values may cause user frustration with legitimate typos.
      • allowMultiplePinVerifications: false (recommended). Impact: Set to false for security - each PIN can only be verified once. Setting to true allows reuse of the same PIN multiple times within the TTL window.
      • pinTimeToLive: How long the OTP is valid (recommended: 5m for 5 minutes). Impact: Shorter TTL increases security but may frustrate users. Longer TTL improves UX but extends vulnerability window.
      • verifyPinLimit: Rate limit for verification attempts (recommended: 1/3s - 1 attempt per 3 seconds). Impact: Prevents brute force attacks while allowing reasonable retry intervals.
      • sendPinPerApplicationLimit: Global rate limit (e.g., 10000/1d - 10,000 per day). Cost Impact: This directly affects your SMS costs. At ~$0.0079 per US SMS, 10,000 daily messages ≈ $79/day or $2,400/month.
      • sendPinPerPhoneNumberLimit: Per-number limit (recommended: 3-5/1d - 3-5 per day per number). Impact: Prevents abuse while allowing legitimate retries. Higher limits increase spam potential and costs.
    • Enabled: Ensure it's set to true.
  4. Click Submit.
  5. Copy the generated Application ID (applicationId). Paste this into INFOBIP_2FA_APP_ID in your .env.local.

2.3 Create a 2FA Message Template:

This defines the content of the SMS message sent to the user, including the OTP placeholder.

  1. Within the 2FA Application you just created, navigate to the Message Templates section.
  2. Click Create Message Template.
  3. Configure the settings:
    • PIN Type: NUMERIC (most common).
    • Message Text: The SMS content. Crucially, include the {{pin}} placeholder where the OTP should be inserted. Example: Your verification code for My App is {{pin}}. It expires in 5 minutes.
    • PIN Length: Number of digits for the OTP (e.g., 6). Remember this value for validation.
    • Sender ID: The name/number displayed as the SMS sender (subject to registration/regulations depending on the country). You might use a default like InfoSMS or configure a custom one if available on your account.
    • Language: Select the language (e.g., en for English).
  4. Click Submit.
  5. Copy the generated Message ID (messageId). Paste this into INFOBIP_2FA_MESSAGE_ID in your .env.local.
  • Why separate Application and Template? This allows you to reuse the same application rules (limits, TTL) with different message templates (e.g., for different languages or slightly different wording for registration vs. login).

3. Building SMS OTP API Endpoints in Next.js

Now, let's create the backend endpoints that our frontend will call.

3.1 Initialize Infobip Client:

It's good practice to create a utility function or singleton instance for the Infobip client.

Create lib/infobip.js (or .ts):

javascript
// lib/infobip.js
import { Infobip } from ""infobip-api-nodejs-sdk"";

let infobipInstance = null;

// Environment variable validation
const validateEnvironment = () => {
  const required = {
    INFOBIP_BASE_URL: process.env.INFOBIP_BASE_URL,
    INFOBIP_API_KEY: process.env.INFOBIP_API_KEY,
    INFOBIP_2FA_APP_ID: process.env.INFOBIP_2FA_APP_ID,
    INFOBIP_2FA_MESSAGE_ID: process.env.INFOBIP_2FA_MESSAGE_ID
  };

  const missing = Object.entries(required)
    .filter(([key, value]) => !value)
    .map(([key]) => key);

  if (missing.length > 0) {
    throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
  }

  return required;
};

export const getInfobipClient = () => {
  if (!infobipInstance) {
    validateEnvironment();
    
    infobipInstance = new Infobip({
      baseUrl: process.env.INFOBIP_BASE_URL,
      apiKey: process.env.INFOBIP_API_KEY,
      // Timeout configuration for production reliability
      timeout: 30000, // 30 seconds timeout
      // Optional: Add retry configuration
      // retryConfig: {
      //   maxRetries: 2,
      //   retryDelay: 1000 // 1 second between retries
      // }
    });
  }
  return infobipInstance;
};

// TypeScript version (create lib/infobip.ts instead):
/*
import { Infobip } from 'infobip-api-nodejs-sdk';

interface InfobipConfig {
  baseUrl: string;
  apiKey: string;
  timeout?: number;
}

interface RequiredEnvVars {
  INFOBIP_BASE_URL: string;
  INFOBIP_API_KEY: string;
  INFOBIP_2FA_APP_ID: string;
  INFOBIP_2FA_MESSAGE_ID: string;
}

let infobipInstance: Infobip | null = null;

const validateEnvironment = (): RequiredEnvVars => {
  const config = {
    INFOBIP_BASE_URL: process.env.INFOBIP_BASE_URL,
    INFOBIP_API_KEY: process.env.INFOBIP_API_KEY,
    INFOBIP_2FA_APP_ID: process.env.INFOBIP_2FA_APP_ID,
    INFOBIP_2FA_MESSAGE_ID: process.env.INFOBIP_2FA_MESSAGE_ID
  };

  const missing = Object.entries(config)
    .filter(([key, value]) => !value)
    .map(([key]) => key);

  if (missing.length > 0) {
    throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
  }

  return config as RequiredEnvVars;
};

export const getInfobipClient = (): Infobip => {
  if (!infobipInstance) {
    validateEnvironment();
    
    const config: InfobipConfig = {
      baseUrl: process.env.INFOBIP_BASE_URL!,
      apiKey: process.env.INFOBIP_API_KEY!,
      timeout: 30000
    };
    
    infobipInstance = new Infobip(config);
  }
  return infobipInstance;
};
*/

// Note on Singletons and Serverless Environments:
// This singleton pattern works well for typical Next.js server deployments.
// However, in serverless environments (like Vercel Serverless Functions),
// the lifecycle might lead to multiple instances or unexpected state retention
// across invocations if not managed carefully. For simple use cases like this,
// it's often acceptable, but be mindful of potential scaling or state issues
// in complex serverless scenarios. Consider initializing the client per-request
// if necessary, though this might have a slight performance overhead.

3.2 API Route: Send OTP (/api/send-otp)

This route receives a phone number, uses the Infobip SDK to send an OTP, and returns the pinId needed for verification.

javascript
// pages/api/send-otp.js
import { getInfobipClient } from ""../../lib/infobip"";
// Consider using a robust phone number validation library for production
// import { parsePhoneNumberFromString } from 'libphonenumber-js';

// Placeholder for a structured logger (replace console calls in production)
// import logger from '../../lib/logger'; // Example: Your logger setup

export default async function handler(req, res) {
  if (req.method !== ""POST"") {
    res.setHeader(""Allow"", [""POST""]);
    return res.status(405).json({ message: `Method ${req.method} Not Allowed` });
  }

  const { phoneNumber } = req.body;

  // --- Phone Number Validation ---
  // Basic Regex (Insecure for Production): Allows 10-15 digits. Doesn't validate country codes.
  const basicPhoneNumberRegex = /^\d{10,15}$/;
  if (!phoneNumber || !basicPhoneNumberRegex.test(phoneNumber)) {
    // Production Recommendation: Use a library like libphonenumber-js
    // const parsedNumber = parsePhoneNumberFromString(phoneNumber); // Requires country code usually
    // if (!parsedNumber || !parsedNumber.isValid()) {
    //    return res.status(400).json({ message: ""Valid E.164 phone number format required (e.g., +14155552671)."" });
    // }
    // Using basic validation for this example:
    return res.status(400).json({ message: ""Valid phone number is required (10-15 digits, include country code)."" });
  }
  // --- End Phone Number Validation ---


  if (!process.env.INFOBIP_2FA_APP_ID || !process.env.INFOBIP_2FA_MESSAGE_ID) {
      // logger.error(""Server configuration error: Infobip App ID or Message ID missing.""); // Use logger
      console.error(""Server configuration error: Infobip App ID or Message ID missing."");
      return res.status(500).json({ message: ""Server configuration error."" });
  }

  try {
    const infobip = getInfobipClient();

    // logger.info(`Attempting to send OTP to ${phoneNumber}`); // Use logger
    const response = await infobip.channels.sms.sendTfaPin({
      applicationId: process.env.INFOBIP_2FA_APP_ID,
      messageId: process.env.INFOBIP_2FA_MESSAGE_ID,
      // 'from' can often be omitted if defined in the template or account defaults
      // from: ""YourSenderID"",
      to: phoneNumber, // Assumes phoneNumber includes country code from input
      // Optional: Define placeholder values if your template uses them beyond {{pin}}
      // placeholders: { ""firstName"": ""User"" }
    });

    // Successfully initiated OTP send
    // logger.info({ msg: ""Infobip Send OTP Response OK"", pinId: response.data.pinId }); // Use logger
    console.log(""Infobip Send OTP Response:"", response.data); // Basic logging

    // IMPORTANT: Return the pinId to the client for the verification step
    return res.status(200).json({ pinId: response.data.pinId });

  } catch (error) {
    const errorData = error.response ? error.response.data : error.message;
    // logger.error({ msg: ""Error sending OTP via Infobip"", error: errorData }); // Use logger
    console.error(""Error sending OTP via Infobip:"", errorData); // Basic logging

    // Provide a generic error to the client, log specifics server-side
    let statusCode = 500;
    let message = ""Failed to send OTP."";

    // Handle specific Infobip error codes if needed
    if (error.response && error.response.data && error.response.data.requestError) {
        const serviceException = error.response.data.requestError.serviceException;
        if (serviceException) {
            message = serviceException.text || message;
            // You could map specific messageIds (like rate limits) to 429 status
            if (serviceException.messageId === 'TOO_MANY_REQUESTS') {
                statusCode = 429;
            }
            // Add other specific error mappings if desired
        }
    } else if (error.response && error.response.status) {
        statusCode = error.response.status; // Use status from Infobip if available
    }

    return res.status(statusCode).json({ message });
  }
}

3.3 API Route: Verify OTP (/api/verify-otp)

This route receives the pinId (obtained from the send step) and the pin entered by the user. It calls Infobip to verify.

javascript
// pages/api/verify-otp.js
import { getInfobipClient } from ""../../lib/infobip"";

// Placeholder for a structured logger (replace console calls in production)
// import logger from '../../lib/logger'; // Example: Your logger setup

export default async function handler(req, res) {
  if (req.method !== ""POST"") {
    res.setHeader(""Allow"", [""POST""]);
    return res.status(405).json({ message: `Method ${req.method} Not Allowed` });
  }

  const { pinId, pin } = req.body;

  // --- Input Validation ---
  if (!pinId) {
    return res.status(400).json({ message: ""Pin ID is required."" });
  }

  // PIN Validation:
  // The regex below assumes a PIN length between 4 and 10 digits.
  // Ideally, this should match the 'PIN Length' configured in your
  // Infobip 2FA Message Template (Section 2.3).
  // Consider making this dynamic or ensuring it matches your config.
  const pinRegex = /^\d{4,10}$/; // Example: Adjust '4' and '10' to match your template's PIN length
  if (!pin || !pinRegex.test(pin)) {
    return res.status(400).json({ message: ""Valid PIN format required."" });
  }
  // --- End Input Validation ---

  try {
    const infobip = getInfobipClient();

    // logger.info(`Attempting to verify OTP for pinId: ${pinId}`); // Use logger
    const response = await infobip.channels.sms.verifyTfaPin(pinId, {
        pin: pin
    });

    // logger.info({ msg: ""Infobip Verify OTP Response OK"", pinId: pinId, verified: response.data.verified }); // Use logger
    console.log(""Infobip Verify OTP Response:"", response.data); // Basic logging

    if (response.data.verified) {
      // OTP Verified Successfully!
      // Perform actions here: e.g., log user in, complete registration, update database flag
      // logger.info(`OTP verification successful for pinId: ${pinId}`); // Use logger
      return res.status(200).json({ verified: true, message: ""OTP verified successfully."" });
    } else {
      // OTP Incorrect or Expired (or other non-verified state)
      // Note: Infobip might return a 200 OK with verified: false for incorrect PINs
      // within the attempt limit. Errors (4xx/5xx) usually indicate other issues.
      // logger.warn(`OTP verification failed for pinId: ${pinId} - Not verified by Infobip.`); // Use logger
      return res.status(400).json({ verified: false, message: ""Invalid or expired OTP."" });
    }

  } catch (error) {
    const errorData = error.response ? error.response.data : error.message;
    // logger.error({ msg: ""Error verifying OTP via Infobip"", pinId: pinId, error: errorData }); // Use logger
    console.error(""Error verifying OTP via Infobip:"", errorData); // Basic logging

    let statusCode = 500;
    let message = ""Failed to verify OTP."";
    let verified = false;

    // Check for specific verification errors (e.g., wrong PIN, expired, max attempts)
    if (error.response && error.response.data && error.response.data.requestError) {
        const serviceException = error.response.data.requestError.serviceException;
         if (serviceException) {
            message = serviceException.text || message; // Use Infobip's error text
            // Map specific errors if needed, e.g., differentiate between wrong PIN and expired PIN
            if (serviceException.messageId === 'PIN_NOT_FOUND') {
                 message = 'OTP session not found or expired.';
                 statusCode = 404; // Not Found is appropriate here
            } else if (serviceException.messageId === 'WRONG_PIN') {
                 message = 'Invalid OTP provided.';
                 statusCode = 400; // Bad Request (client error)
            } else if (serviceException.messageId === 'MAX_ATTEMPTS_EXCEEDED') {
                message = 'Maximum verification attempts reached.';
                statusCode = 429; // Too Many Requests
            } else {
                // Use Infobip's status code if available and seems appropriate
                 statusCode = error.response.status >= 400 ? error.response.status : 400;
            }
         }
    } else if (error.response && error.response.status) {
        statusCode = error.response.status;
    }


    return res.status(statusCode).json({ verified, message });
  }
}

API Testing (Example using curl):

Replace placeholders with actual values. Remember to start your Next.js development server (npm run dev or yarn dev).

  1. Send OTP:

    bash
    curl -X POST http://localhost:3000/api/send-otp \
    -H ""Content-Type: application/json"" \
    -d '{""phoneNumber"": ""+14155552671""}' # Use a real number you can receive SMS on, including country code

    Expected Response (Success): {""pinId"":""SOME_PIN_ID_FROM_INFOBIP""} Expected Response (Error): { ""message"": ""Specific error message..."" } (with appropriate status code)

  2. Verify OTP (using pinId from above and the received SMS code):

    bash
    curl -X POST http://localhost:3000/api/verify-otp \
    -H ""Content-Type: application/json"" \
    -d '{""pinId"": ""SOME_PIN_ID_FROM_INFOBIP"", ""pin"": ""123456""}' # Replace 123456 with the actual OTP

    Expected Response (Success): {""verified"": true, ""message"": ""OTP verified successfully.""} Expected Response (Invalid PIN): {""verified"": false, ""message"": ""Invalid or expired OTP.""} (or more specific error)

4. Creating the OTP Verification UI Components

Now, let's create a simple UI for users to interact with our OTP flow.

jsx
// pages/index.js
import React, { useState } from 'react';

// Frontend Styling Note:
// This example uses basic inline styles (`style={{ ... }}`) for simplicity.
// For production applications, it's highly recommended to use more maintainable
// styling solutions like CSS Modules, Tailwind CSS, styled-components, or Emotion,
// which integrate well with Next.js.

export default function OtpPage() {
  const [phoneNumber, setPhoneNumber] = useState('');
  const [pinId, setPinId] = useState(''); // State to hold the pinId from the backend
  const [otp, setOtp] = useState('');
  const [message, setMessage] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [isOtpSent, setIsOtpSent] = useState(false); // Controls which form part is shown
  const [isVerified, setIsVerified] = useState(false); // Controls final success message

  const handleSendOtp = async (e) => {
    e.preventDefault();
    setMessage('');
    setIsLoading(true);
    setIsVerified(false);
    setIsOtpSent(false); // Reset OTP state
    setPinId('');       // Reset pinId

    // Frontend Input Consideration:
    // While backend validation is essential, consider adding basic frontend
    // format masking (e.g., using react-imask) or validation for the phone number
    // input to improve user experience and reduce invalid submissions.

    try {
      const res = await fetch('/api/send-otp', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ phoneNumber }),
      });

      const data = await res.json();

      if (res.ok) {
        setPinId(data.pinId); // Store the pinId received from the API
        setIsOtpSent(true);
        setMessage('OTP sent successfully! Please check your phone.');
      } else {
        setMessage(`Error: ${data.message || 'Failed to send OTP'}`);
        setIsOtpSent(false);
      }
    } catch (error) {
      console.error(""Frontend Error Sending OTP:"", error);
      setMessage('An unexpected error occurred. Please try again.');
      setIsOtpSent(false);
    } finally {
      setIsLoading(false);
    }
  };

  const handleVerifyOtp = async (e) => {
    e.preventDefault();
    setMessage('');
    setIsLoading(true);

    try {
      const res = await fetch('/api/verify-otp', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ pinId, pin: otp }), // Send pinId and the entered OTP
      });

      const data = await res.json();

      if (res.ok && data.verified) {
        setIsVerified(true);
        setMessage('Success! Phone number verified.');
        // Optionally reset the form or navigate the user
        setPinId('');
        setOtp('');
        setIsOtpSent(false); // Go back to phone number input stage for next time
        // setPhoneNumber(''); // Optional: clear phone number too
      } else {
        // Handle verification failure (invalid OTP, expired, etc.)
        setMessage(`Error: ${data.message || 'Failed to verify OTP'}`);
        setIsVerified(false);
        setOtp(''); // Clear OTP input field for retry attempt
      }
    } catch (error) {
       console.error(""Frontend Error Verifying OTP:"", error);
      setMessage('An unexpected error occurred during verification.');
      setIsVerified(false);
    } finally {
      setIsLoading(false);
    }
  };

  // Handler for the ""Change/Resend"" button
  const handleResetFlow = () => {
    setIsOtpSent(false); // Go back to the phone number input stage
    setMessage('');
    setOtp('');
    setPinId('');
    setIsVerified(false);
    // The phone number input remains populated, allowing easy resend or correction.
  };


  return (
    <div style={{ maxWidth: '400px', margin: '50px auto', padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
      <h1>Phone Verification</h1>

      {/* Show forms only if not successfully verified */}
      {!isVerified && (
        <form onSubmit={!isOtpSent ? handleSendOtp : handleVerifyOtp}>
          {!isOtpSent ? (
            // Stage 1: Enter Phone Number
            <>
              <label htmlFor=""phoneNumber"" style={{ display: 'block', marginBottom: '5px' }}>Phone Number:</label>
              <input
                type=""tel"" // Use ""tel"" type for better mobile UX
                id=""phoneNumber""
                value={phoneNumber}
                onChange={(e) => setPhoneNumber(e.target.value)}
                placeholder=""e.g., +14155552671 (with country code)""
                required
                disabled={isLoading}
                style={{ width: '100%', padding: '8px', marginBottom: '10px', boxSizing: 'border-box' }}
              />
              <button type=""submit"" disabled={isLoading} style={{ width: '100%', padding: '10px' }}>
                {isLoading ? 'Sending...' : 'Send OTP'}
              </button>
            </>
          ) : (
            // Stage 2: Enter OTP
            <>
              <p>Enter the OTP sent to {phoneNumber}.</p>
              <label htmlFor=""otp"" style={{ display: 'block', marginBottom: '5px' }}>OTP:</label>
              <input
                type=""text"" // Use ""text"" and pattern/maxLength for better control
                inputMode=""numeric"" // Hint for numeric keyboard on mobile
                id=""otp""
                value={otp}
                onChange={(e) => setOtp(e.target.value)}
                maxLength={6} // Match your PIN length configured in Infobip
                pattern=""\d*"" // Allow only digits
                required
                disabled={isLoading}
                autoComplete=""one-time-code"" // Helps browsers/password managers autofill OTP
                style={{ width: '100%', padding: '8px', marginBottom: '10px', boxSizing: 'border-box' }}
              />
               <button type=""submit"" disabled={isLoading} style={{ width: '100%', padding: '10px', marginBottom: '10px' }}>
                {isLoading ? 'Verifying...' : 'Verify OTP'}
              </button>
              {/* Button to go back to phone input / allow resend */}
              <button
                type=""button""
                onClick={handleResetFlow}
                disabled={isLoading}
                style={{ width: '100%', padding: '10px', background: '#eee', border: '1px solid #ccc', cursor: 'pointer' }}
                >
                 Change Phone Number / Resend OTP
              </button>
            </>
          )}
        </form>
      )}

      {/* Display messages (errors or success) */}
      {message && (
        <p style={{ marginTop: '15px', color: isVerified ? 'green' : (message.startsWith('Error:') ? 'red' : 'black') }}>
          {message}
        </p>
      )}
    </div>
  );
}

5. Production Error Handling and Monitoring

  • API Routes: We've included try...catch blocks.
    • Production Logging: Replace console.log and console.error with a dedicated, structured logging library (e.g., Pino, Winston) or integrate with a logging service (e.g., Sentry, Logtail, Datadog). This allows for better filtering, searching, and alerting in production. Log detailed error information server-side, including Infobip's response data when available (error.response.data).
    • User Messages: Return user-friendly, non-revealing error messages to the client (e.g., ""Invalid OTP"" instead of exposing internal error codes).
    • Infobip Errors: Handle specific Infobip errors (rate limits 429, invalid credentials 401, etc.) by checking error.response.status and error.response.data.requestError.serviceException.messageId.
  • Frontend: Use try...catch around fetch calls. Display clear messages to the user based on API responses. Provide ways to retry (like the ""Resend OTP"" button) or restart the process.
  • Retry Mechanisms: For transient network issues calling Infobip from your backend, you could implement a simple retry in the API route (e.g., retry once after a short delay using libraries like async-retry). Be cautious with retrying OTP sends to avoid spamming users or hitting rate limits quickly. Exponential backoff is generally recommended for retrying external API calls.

6. SMS Authentication Security Best Practices

  • API Key Security: Absolutely crucial. Use environment variables (.env.local for local development) and never expose your API key in frontend code or commit it to version control. Configure restricted permissions (least privilege principle) and consider IP allowlisting for your Infobip API key in production.
  • Rate Limiting:
    • Infobip: Configure sensible limits (verifyPinLimit, sendPinPerPhoneNumberLimit, sendPinPerApplicationLimit) in your Infobip 2FA Application settings to prevent abuse at the source.
    • Your API Routes: Implement rate limiting on your /api/send-otp and /api/verify-otp endpoints to protect your own server resources. Libraries like rate-limiter-flexible or express-rate-limit (adapted for Next.js API routes) can be used. Hosting platforms like Vercel also offer built-in rate limiting features.
  • Input Validation: Sanitize and rigorously validate all inputs (phone number format, PIN format, pinId presence) on the server-side (API routes). Do not rely solely on frontend validation, as it can be bypassed. Use robust libraries like libphonenumber-js for phone numbers.
  • PIN ID Handling (Security Trade-off):
    • Current Implementation: This guide sends the pinId to the client and back. This is simpler to implement but less secure, as the pinId is exposed. An attacker who obtains a pinId could potentially attempt to guess the PIN by directly calling your /api/verify-otp endpoint (though Infobip's rate limits offer protection).
    • Recommended Production Pattern (More Secure): Do not send the pinId to the client. Instead, store the pinId server-side immediately after the /api/send-otp call. Associate it with the user's session (e.g., using next-auth or encrypted cookies) or a temporary secure cache (like Redis) keyed by session ID or phone number, with a TTL matching Infobip's pinTimeToLive. When the user submits the OTP to /api/verify-otp, retrieve the correct pinId from the server-side storage based on their session/context and then call Infobip. This prevents pinId exposure but adds server-side state management complexity.
  • Brute Force Protection: Infobip's pinAttempts setting provides primary protection against guessing the OTP. Your API route rate limiting adds another layer of defense against hammering the verification endpoint.
  • HTTPS: Always use HTTPS for your application to encrypt data in transit between the client, your server, and Infobip. Platforms like Vercel handle this automatically.
  • CSRF Protection: Next.js API routes generally require same-origin requests, offering some CSRF protection. Ensure you aren't disabling security features and understand CSRF risks if using complex authentication flows or custom headers.

7. Testing SMS OTP Implementation

  • Infobip Test Numbers/Sandbox: Check the current Infobip documentation regarding sandbox environments or dedicated test numbers. While sometimes available, they might have limitations. Often, developers need to use real phone numbers for testing the end-to-end SMS delivery flow, which may incur small costs. Ensure you understand any associated costs or limitations.
  • Unit Tests: Use testing frameworks like Jest or Vitest to write unit tests for your API routes (send-otp.js, verify-otp.js). Mock the infobip-api-nodejs-sdk using jest.mock or similar techniques to simulate:
    • Successful OTP send responses (returning a mock pinId).
    • Successful OTP verification responses (verified: true).
    • Failed OTP verification responses (verified: false).
    • Specific Infobip error responses (invalid PIN, expired, rate limit, invalid API key).
    • Network errors during the Infobip API call.
    • Test your input validation logic thoroughly (valid/invalid phone numbers, PINs, missing pinId).
  • Integration Tests: Test the flow between your frontend and backend API routes. Use tools like Cypress or Playwright to simulate user interaction: entering a phone number, clicking ""Send OTP"", receiving a (mocked or real) OTP, entering it, and clicking ""Verify OTP"". Mock the fetch calls in your frontend tests or run against a live development server (potentially mocking the Infobip SDK at the API level).
  • End-to-End (E2E) Tests: For critical flows, consider E2E tests that interact with a staging environment connected to Infobip (using test credentials and numbers if possible).

This validates the entire system, including the actual Infobip integration, but can be more complex and costly to set up and run.

8. Deploying Your Next.js OTP System to Production

When deploying your SMS OTP authentication system, consider these platform-specific configurations:

Vercel Deployment (Recommended for Next.js):

  • Environment variables: Add all INFOBIP_* variables in Vercel dashboard under Project Settings → Environment Variables
  • Automatic HTTPS: Vercel provides SSL certificates automatically
  • Serverless function limits: Default 10-second timeout (sufficient for Infobip API calls)
  • Regional deployment: Choose regions close to your Infobip API region for optimal latency

Environment Variable Management:

bash
# Production deployment checklist
vercel env add INFOBIP_BASE_URL production
vercel env add INFOBIP_API_KEY production
vercel env add INFOBIP_2FA_APP_ID production
vercel env add INFOBIP_2FA_MESSAGE_ID production

Performance Optimization:

  • Enable Next.js caching for the Infobip client singleton
  • Implement connection pooling for database operations (if storing verification logs)
  • Monitor API response times via Vercel Analytics or custom logging
  • Set appropriate timeout values (30 seconds recommended for SMS API calls)

Monitoring and Alerting:

  • Track OTP send success rates
  • Monitor verification attempt patterns for abuse detection
  • Set up alerts for API rate limit approaches
  • Log all security-related events (failed verifications, rate limit hits)

9. Common Issues and Troubleshooting

Issue: "Missing required environment variables" error

  • Solution: Verify all four Infobip environment variables are set in .env.local and properly loaded by Next.js
  • Check: Restart development server after adding environment variables

Issue: SMS not received by users

  • Verify phone number format includes country code (E.164 format: +14155552671)
  • Check Infobip account balance and SMS sending limits
  • Review Infobip portal logs for delivery status and error codes
  • Confirm phone number is not on a blocklist or belongs to a restricted region

Issue: "Invalid credentials" or 401 errors

  • Regenerate API key in Infobip portal and update .env.local
  • Verify API key has "TFA" scope permissions enabled
  • Check Base URL matches your account region (e.g., xxxxx.api.infobip.com)

Issue: Rate limit errors (429 status)

  • Review sendPinPerPhoneNumberLimit and sendPinPerApplicationLimit settings in Infobip 2FA Application
  • Implement exponential backoff retry logic
  • Add user-side rate limiting UI feedback ("Please wait X seconds before requesting new code")

Issue: OTP verification always fails

  • Confirm PIN length in code validation matches Infobip template configuration
  • Check pinTimeToLive setting—OTP may be expiring too quickly
  • Verify pinId is correctly passed from send to verify API route
  • Review Infobip portal logs for specific verification failure reasons

10. Next Steps and Advanced Features

Enhance Your Implementation:

  • Multi-channel verification: Add email OTP alongside SMS for backup verification method
  • Internationalization: Create multiple message templates for different languages
  • Custom sender IDs: Register branded sender IDs for improved deliverability and trust
  • Fallback mechanisms: Implement voice call OTP for users who don't receive SMS

Security Hardening:

  • Implement Redis-based session storage for server-side pinId management
  • Add device fingerprinting to detect suspicious verification patterns
  • Enable fraud detection rules in Infobip portal
  • Integrate with threat intelligence APIs to block known bad actors

User Experience Improvements:

  • Auto-fill OTP support using WebOTP API (navigator.credentials.get())
  • Show remaining time countdown for OTP expiration
  • Implement progressive disclosure for phone number input with country selector
  • Add accessibility features (ARIA labels, screen reader support)

Related Guides: