Vonage 2FA Tutorial: Implement OTP Authentication in Node.js Express
Build Two-Factor Authentication (2FA) with Vonage Verify API in Node.js Express. Step-by-step guide for SMS OTP verification with code examples, error handling, and production considerations.
Add Two-Factor Authentication (2FA) via SMS One-Time Passwords (OTP) to your Node.js Express application using the Vonage Verify API. Build a simple application that requests an OTP for a user's phone number and verifies the code they enter. Use this guide as a starting point, demonstrating core concepts and production considerations.
Enhance your security by requiring users to possess their phone to receive a code – a critical layer beyond password authentication.
Project Goals:
Build a Node.js Express application demonstrating OTP request and verification.
Securely integrate the Vonage Verify API for sending and checking OTPs.
Implement basic error handling and user feedback mechanisms.
Provide a foundation for adding 2FA to existing or new web applications, highlighting key differences between demo setup and production requirements.
Technologies Used:
Node.js: JavaScript runtime environment. This guide uses Node.js 22.x LTS (Active LTS until October 2025, Maintenance until April 2027) or Node.js 20.x LTS (Maintenance until April 2026).
Express.js: Minimalist web framework for Node.js. This guide uses Express 5.1.0 or later (requires Node.js 18+).
Vonage Verify API: Service for sending and verifying OTPs via SMS and voice calls. Vonage (formerly Nexmo, rebranded 2020) remains active as of January 2025.
@vonage/server-sdk: Official Vonage Node.js library for interacting with the API.
Nunjucks: Templating engine for rendering HTML views (similar to Jinja2).
dotenv: Module for loading environment variables from a .env file.
Node.js and npm (or yarn): Install on your development machine. Download Node.js
Vonage API Account: Sign up for a free account at Vonage API Dashboard. You'll receive free credits to start.
Vonage API Key and Secret: Find these at the top of your Vonage API Dashboard after signing up.
Basic understanding of Node.js, Express, and asynchronous JavaScript (Promises, async/await).
1. Set Up Your Project
Initialize your Node.js project and install the necessary dependencies.
1. Create Project Directory:
Open your terminal and create a new directory for the project, then navigate into it:
Bash
mkdir vonage-otp-express
cd vonage-otp-express
2. Initialize Node.js Project:
Create a package.json file to manage dependencies and project metadata:
Bash
npm init -y
3. Install Dependencies:
Install Express, the Vonage SDK, Nunjucks for templating, and dotenv for managing environment variables. Express includes built-in middleware for body parsing, so body-parser is not needed as a separate dependency.
Replace YOUR_API_KEY and YOUR_API_SECRET with the actual values from your Vonage Dashboard.
6. Configure Git Ignore (.gitignore):
Create a .gitignore file to prevent committing sensitive information and unnecessary files:
.gitignore
Text
node_modules/
.env
npm-debug.log
7. Basic Express Server Setup (index.js):
Create the main application file index.js and set up the initial Express server, environment variables, middleware, and Nunjucks configuration.
index.js
JavaScript
// 1. Import dependenciesrequire('dotenv').config(); // Load environment variables from .env file first (ensure it's in project root)const express = require('express');
const nunjucks = require('nunjucks');
const { Vonage } = require('@vonage/server-sdk');
// --- Basic Application Setup ---const app = express();
const port = process.env.PORT || 3000; // Use port from env or default to 3000// --- Vonage Client Initialization ---// Check if API Key and Secret are setif (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET) {
console.error('❌ Missing Vonage API Key or Secret in .env file. Check your configuration.');
process.exit(1); // Exit if credentials are missing
}
const vonage = newVonage({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET
});
console.log('✅ Vonage client initialized.');
// --- Middleware Setup ---// Use built-in Express middleware for parsing JSON and URL-encoded bodies
app.use(express.json()); // Parse JSON bodies (as sent by API clients)
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies (as sent by HTML forms)// --- Nunjucks Templating Engine Setup ---
nunjucks.configure('views', {
autoescape: true, // Enable automatic escaping to prevent XSSexpress: app // Link Nunjucks to the Express app
});
app.set('view engine', 'html'); // Set default view engine extensionconsole.log('✅ Middleware and Templating Engine configured.');
// --- Temporary Storage for Request IDs ---// IMPORTANT: This in-memory object is for demonstration purposes ONLY.// It is NOT suitable for production environments because:// 1. Data is lost when the server restarts.// 2. It does not scale across multiple server instances.// See Section 6 for production-ready storage (e.g., Redis, Database).const verificationRequests = {};
console.warn("⚠️ Using in-memory store for verification requests. This is NOT production-ready. Data will be lost on restart.");
// --- Routes Will Go Here ---// GET /// POST /request-otp// POST /verify-otp// GET /success// GET /error// --- Start Server ---
app.listen(port, () => {
console.log(`🚀 Server listening on http://localhost:${port}`);
});
// Basic error handling middleware (optional but recommended)
app.use((err, req, res, next) => {
console.error("❌ Global Error Handler:", err.stack);
res.status(500).render('error.html', { errorMessage: 'An unexpected server error occurred.' });
});
Explanation:
require('dotenv').config(): Loads variables from .env into process.env. Ensure the .env file is in the project root.
express(), nunjucks: Standard setup.
Vonage Initialization: Creates the client instance using credentials from .env. Includes a check for credential existence.
Middleware: express.json() and express.urlencoded() are built-in Express middleware for parsing request bodies. extended: true allows for rich objects and arrays to be encoded.
Nunjucks Config: Configures the template engine. autoescape: true is crucial for security.
verificationRequests: This simple in-memory object is for demonstration only. It maps phone numbers to their pending request_id. It is explicitly not suitable for production. Section 6 discusses proper persistent storage solutions.
app.listen: Starts the server.
Run node index.js. You should see console logs indicating successful initialization. Visiting http://localhost:3000 will initially fail because routes aren't defined yet.
2. Create Frontend Views
Create the simple HTML pages for user interaction. (Inline styles are used for simplicity; external CSS is recommended for larger applications).
Add this inside index.js where // --- Routes Will Go Here --- is marked:
JavaScript
// --- Routes ---// Render the initial page to enter phone number
app.get('/', (req, res) => {
res.render('index.html');
});
2. Request OTP Route (POST /request-otp):
Handles phone number submission, calls Vonage to send the OTP, stores the request_id (temporarily), and renders the verification page.
Add this below the GET / route in index.js:
JavaScript
// Handle the request to send an OTP
app.post('/request-otp', async (req, res) => {
const phoneNumber = req.body.phoneNumber;
// Basic input validationif (!phoneNumber) {
return res.status(400).render('index.html', { errorMessage: 'Phone number is required.' });
}
// Basic format check (digits only, length 10 – 15).// WARNING: This regex is simplistic and NOT robust for validating international E.164 formats.// Use a library like 'libphonenumber-js' for proper validation in production (See Section 8).if (!/^\d{10,15}$/.test(phoneNumber)) {
return res.status(400).render('index.html', { errorMessage: 'Invalid phone number format. Include country code without symbols (e.g., 14155552671).' });
}
console.log(`📱 Requesting OTP for ${phoneNumber}...`);
try {
// Start the Vonage Verify request// Defaults: 4-digit code, 5-minute expiry. See optional params below.const response = await vonage.verify.start({
number: phoneNumber,
brand: "MyAppVerification", // Customize your brand name shown in the SMS// code_length: '6', // Optional: Specify '6' for 6-digit code// workflow_id: 6 // Optional: Specify workflows (e.g., 6 for SMS only)// See Vonage docs for workflow IDs
});
// Check if the request was successful based on Vonage response structureif (response.request_id) {
const requestId = response.request_id;
console.log(`✅ OTP Request successful. Request ID: ${requestId}`);
// Store the request ID associated with the phone number (TEMPORARY IN-MEMORY STORAGE)
verificationRequests[phoneNumber] = requestId;
console.log("📝 Stored Request ID (In-Memory):", verificationRequests);
// Render the page to enter the OTP code
res.render('verify.html', { requestId: requestId, phoneNumber: phoneNumber });
} else {
// This block might be reached if the Vonage API structure changes or returns unexpected success formatconsole.error("⚠️ Unexpected Vonage response structure:", response);
res.render('error.html', { errorMessage: 'Failed to initiate OTP request. Unexpected response.' });
}
} catch (error) {
console.error("❌ Error requesting OTP:", error);
// Handle specific Vonage errors and other issueslet userErrorMessage = 'An error occurred while sending the verification code.';
// Check both HTTP status from the error object and Vonage-specific status in the response dataconst httpStatus = error.response?.status;
const vonageErrorData = error.response?.data;
if (vonageErrorData) {
console.error(`Vonage Error Details (HTTP Status: ${httpStatus}):`, vonageErrorData);
// Example: Handle throttling or invalid number errors specificallyif (vonageErrorData.status === '10') { // Throttled
userErrorMessage = "Wait a moment before requesting another code for this number.";
} elseif (vonageErrorData.status === '3') { // Invalid number
userErrorMessage = "The phone number provided is invalid. Check and try again.";
} else {
userErrorMessage = `Verification failed: ${vonageErrorData.error_text} (Status: ${vonageErrorData.status})`;
}
} elseif (httpStatus) {
userErrorMessage = `Failed to send code. Server responded with status ${httpStatus}.`;
} // Else: Generic error message remains// Render the index page again with an error message
res.status(httpStatus || 500).render('index.html', { errorMessage: userErrorMessage });
}
});
3. Verify OTP Route (POST /verify-otp):
Handles code submission, calls Vonage to check the code against the request_id, and renders success or error pages.
Add this below the POST /request-otp route in index.js:
JavaScript
// Handle the OTP verification
app.post('/verify-otp', async (req, res) => {
const otpCode = req.body.otpCode;
const requestId = req.body.requestId;
const phoneNumber = req.body.phoneNumber; // Retrieve phone number// Basic validationif (!otpCode || !requestId || !phoneNumber) {
return res.status(400).render('error.html', { errorMessage: 'Missing OTP code, request ID, or phone number.' });
}
if (!/^\d{4,6}$/.test(otpCode)) {
return res.status(400).render('verify.html', {
errorMessage: 'Invalid OTP format. Enter the 4 or 6 digit code.',
requestId: requestId, // Pass context back to the formphoneNumber: phoneNumber
});
}
console.log(`🔍 Verifying OTP ${otpCode} for Request ID: ${requestId}...`);
try {
// Check the OTP code with Vonageconst result = await vonage.verify.check(requestId, otpCode);
console.log("📋 Vonage Check Response:", result);
// Check the status from the Vonage responseif (result && result.status === '0') {
// Status '0' means successful verificationconsole.log(`✅ Verification successful for Request ID: ${requestId}`);
// IMPORTANT: Clear the stored request ID after successful verification (from temporary store)if (verificationRequests[phoneNumber]) {
delete verificationRequests[phoneNumber];
console.log("🗑️ Cleared Request ID for:", phoneNumber, "(In-Memory)");
}
res.render('success.html');
} else {
// Verification failed (wrong code, expired, etc.)const errorMessage = result.error_text || `Verification failed with status: ${result.status}`;
console.warn(`⚠️ Verification failed for Request ID: ${requestId}. Reason: ${errorMessage}`);
// Render the verification page again with an error
res.status(400).render('verify.html', {
errorMessage: `Verification Failed: ${errorMessage}`,
requestId: requestId, // Pass context back to the formphoneNumber: phoneNumber
});
}
} catch (error) {
console.error("❌ Error verifying OTP:", error);
let userErrorMessage = 'An error occurred during verification.';
const httpStatus = error.response?.status;
const vonageErrorData = error.response?.data;
if (vonageErrorData) {
console.error(`Vonage Error Details (HTTP Status: ${httpStatus}):`, vonageErrorData);
// Example: Specific handling for error status codes from check endpointif (vonageErrorData.status === '16') { // Incorrect code
userErrorMessage = "The code you entered was incorrect. Try again.";
} elseif (vonageErrorData.status === '17') { // Wrong code too many times
userErrorMessage = "You entered the wrong code too many times. Request a new code.";
} elseif (vonageErrorData.status === '6') { // Request not found / expired
userErrorMessage = "Verification request expired or was not found. Request a new code.";
} else {
userErrorMessage = `Verification error: ${vonageErrorData.error_text} (Status: ${vonageErrorData.status})`;
}
// Render the verification page again with the error
res.status(httpStatus || 500).render('verify.html', {
errorMessage: userErrorMessage,
requestId: requestId, // Pass context backphoneNumber: phoneNumber
});
} else {
// Render a generic error page if no specific Vonage error info
res.status(httpStatus || 500).render('error.html', { errorMessage: userErrorMessage });
}
}
});
// Add placeholder routes for direct access to success/error (optional)
app.get('/success', (req, res) => res.render('success.html'));
app.get('/error', (req, res) => res.render('error.html', { errorMessage: 'An unspecified error occurred.'}));
Explanation:
Input Validation: Added basic checks. Robust validation (e.g., express-validator) is recommended for production. The phone regex is overly simple.
vonage.verify.start: Initiates OTP. brand appears in SMS. Default code length (4) and expiry (5 minutes) are mentioned.
request_id Storage: Temporarily stores the ID in the insecure verificationRequests object.
vonage.verify.check: Verifies the code against the ID.
Status '0': Indicates success. Other statuses are errors.
Error Handling:try...catch blocks handle errors. Specific Vonage statuses (3, 6, 10, 16, 17) and HTTP status codes (error.response.status) are checked for better feedback.
Cleanup: Removes the entry from verificationRequests on success. Essential for the demo, but real storage needs proper management (TTL, etc.).
4. Integrate Vonage Verify API (Recap and Details)
Network/SDK Errors: Log these comprehensively. Check error.response.status (HTTP status code) in addition to error.response.data.status (Vonage status) for more context.
Logging: Use dedicated libraries (Winston, Pino) for structured JSON logging, log levels, and appropriate transports (console, file, external service). Include context (request IDs, user identifiers – anonymize PII if needed).
Retry Mechanisms: Implement retries with backoff for transient network errors (e.g., using async-retry), especially for verify.check. Be cautious retrying verify.start due to throttling.
6. Production Considerations: Persistent Storage
The in-memory verificationRequests object used in this guide is strictly for demonstration and NOT suitable for production.
Why In-Memory Storage Fails in Production:
Data Loss: Server restarts wipe all pending verifications.
Scalability: Cannot work with multiple server instances (load balancing). Each instance would have its own separate, inconsistent memory store.
Production Solutions:
Redis: Highly recommended for this use case. It's fast and designed for temporary, expiring data.
Store request_id keyed by phone number (or vice-versa).
Set a Time-To-Live (TTL) matching the Vonage expiry (e.g., 5 minutes). Redis auto-deletes expired keys.
Database (SQL/NoSQL): Use your application's main database (PostgreSQL, MongoDB, etc.).
Create a VerificationRequests table/collection.
Store request_id, phoneNumber, createdAt, expires_at, status ('pending', 'verified', etc.).
Query based on request_id for verification.
Implement cleanup logic (e.g., a scheduled job) to remove old/expired entries based on expires_at.
Conceptual Schema (SQL Example):
SQL
CREATE TABLE VerificationRequests (
request_id VARCHAR(50) PRIMARY KEY, -- Vonage Request ID
phone_number VARCHAR(20) NOT NULL,
status VARCHAR(10) NOT NULLDEFAULT'pending', -- 'pending', 'verified', 'failed', 'expired'
created_at TIMESTAMPWITHTIME ZONE DEFAULTCURRENT_TIMESTAMP,
expires_at TIMESTAMPWITHTIME ZONE NOT NULL, -- Set based on Vonage expiry (e.g., NOW() + INTERVAL '5 minutes')
attempts INTDEFAULT0
);
CREATE INDEX idx_verification_phone ON VerificationRequests (phone_number);
CREATE INDEX idx_verification_expires ON VerificationRequests (expires_at);
Implementation: Replace verificationRequests[key] = value and delete verificationRequests[key] with calls to your chosen persistent store (e.g., using redis, pg, mongoose). Handle potential data store errors.
7. Adding Security Features
Essential security practices:
Input Validation: Use robust libraries (express-validator, joi) for strict validation of phone numbers and OTP codes. Sanitize inputs.
Rate Limiting: Crucial to prevent abuse and brute-force attacks. Use express-rate-limit or similar.
Limit OTP requests per phone number/IP.
Limit verification attempts per request_id/IP.
Secure Secret Management: Use cloud provider secret managers (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) or inject environment variables securely via CI/CD in production. Never commit .env or secrets.
HTTPS: Enforce HTTPS in production.
Session Management: If integrating into a login flow, use secure session practices (express-session with a secure store, secure cookie attributes: HttpOnly, Secure, SameSite).
8. Handling Special Cases
Phone Number Formatting: Vonage requires E.164 format. Use libraries like libphonenumber-js for reliable parsing, validation, and formatting on the server-side. Guide users in the UI.
International Numbers: Supported by Vonage, but ensure validation handles them. Be aware of potential cost differences.
Concurrent Requests (Throttling): Vonage status 10 means too many requests to the same number recently (~30 second window). Inform the user to wait. Check your persistent store before calling Vonage to see if a recent request for that number is still pending.
Code Expiry: Default is 5 minutes (can be configured via API or dashboard). Inform users. Check expiry (expires_at in DB or rely on Redis TTL) before attempting verify.check. Handle status 6 (not found/expired) gracefully.
Resend Functionality:
Add a "Resend Code" option.
Important: Before calling vonage.verify.start for a resend, call vonage.verify.cancel(previous_request_id) if a previous request for the same number is still active within Vonage's system (typically within the expiry window). This prevents users from having multiple potentially valid codes active simultaneously. Retrieve the previous_request_id from your persistent store.
Generate a newrequest_id with the resend call.
Update your persistent store with the new request_id and reset the expires_at timestamp.
Apply rate limiting to the resend action itself.
SMS vs. Voice: Use the workflow_id parameter in verify.start to customize delivery (e.g., SMS only, voice only).
9. Implementing Performance Optimizations
Data Store Speed: Redis is typically fastest for this. Optimize database indexes (request_id, phone_number, expires_at).
Asynchronous Operations: Ensure all I/O (Vonage API, data store) uses async/await or Promises correctly.
Load Testing: Use tools (k6, artillery) to test performance under load. Monitor Vonage API latency.
10. Adding Monitoring, Observability, and Analytics
Understand system health and performance:
Health Checks:/health endpoint checking dependencies.
Performance Metrics: Track request latency, Vonage API latency, success/failure rates, rate limit counts (Prometheus/Grafana, Datadog, New Relic).
Error Tracking: Use services (Sentry, Bugsnag) to capture and alert on errors.
Dashboards: Visualize key metrics.
Alerting: Set alerts for high error rates, latency spikes, health check failures.
SDK/API Version Issues: Keep SDK updated, check Vonage changelogs.
In-Memory Store Issues (Demo): This guide's use of verificationRequests = {} is only for demonstration and will fail in production or multi-instance environments. Use persistent storage (Section 6).
Environment Variables: Configure VONAGE_API_KEY, VONAGE_API_SECRET, and database/Redis credentials securely in the deployment environment. Do not commit secrets.
PORT Variable: Use process.env.PORT.
Build Process:npm install --production.
Starting the App: Use pm2 or platform mechanisms (Procfile, Dockerfile CMD).
CI/CD Pipeline: Automate testing, building, and deployment. Inject secrets securely.
Rollback Plan: Ensure you can revert to previous versions easily.
Example Dockerfile (Conceptual):
Dockerfile
# Use an appropriate Node.js base image
FROM node:18-alpine AS base
WORKDIR /app
# Copy package files
COPY package.json package-lock.json ./
# Install production dependencies only
RUN npm ci --only=production
# Copy the rest of the application code
COPY . .
# Expose the port the app runs on (should match server config)
EXPOSE 3000
# Define the command to run the application
CMD [ "node", "index.js" ]
13. Verification and Testing
Ensure correctness:
1. Manual Verification Steps:
Start server: node index.js.
Open http://localhost:3000.
Enter a valid test phone number (E.164 format).
Submit → Check phone for SMS → Verify page loads.
Enter correct code → Submit → Success page loads.
Test failures: Invalid phone format, wrong code, expired code (>5 minutes), too many wrong codes, missing code.
Implement unit tests (Jest, Mocha) for individual functions/modules and integration tests (Supertest) to test API endpoints and workflows. Mock the Vonage API calls during testing.
14. Frequently Asked Questions (FAQ)
How do I implement Two-Factor Authentication with Vonage in Node.js?
Implement Two-Factor Authentication with Vonage by: 1) Installing the @vonage/server-sdk package, 2) Initializing the Vonage client with your API credentials, 3) Calling vonage.verify.start() to send an OTP to the user's phone, 4) Storing the returned request_id, and 5) Verifying the user-entered code with vonage.verify.check(). Use Express routes to handle the request and verification flows, and implement persistent storage (Redis or database) for production environments.
What is the Vonage Verify API and how does it work?
The Vonage Verify API is a service that sends One-Time Passwords (OTPs) via SMS or voice call to verify user phone numbers. When you call vonage.verify.start(), Vonage sends a code to the specified phone number and returns a request_id. Users enter the received code, which you then verify by calling vonage.verify.check() with the request_id and code. A status of '0' indicates successful verification, while non-zero statuses indicate errors like incorrect codes or expired requests.
What storage should I use for Vonage request IDs in production?
Use Redis or a database (PostgreSQL, MongoDB) for storing Vonage request IDs in production – never in-memory storage. Redis is optimal for this use case because it supports automatic expiration (TTL) matching Vonage's 5-minute default expiry, provides fast lookups, and works across multiple server instances. Database storage requires implementing your own cleanup logic but integrates well with existing application data. Store the request_id, phone number, creation timestamp, and expiry time.
How do I handle Vonage Verify API error codes?
Handle Vonage Verify API errors by checking the status field in responses: Status '0' = success, status '3' = invalid number, status '6' = expired/not found request, status '10' = throttled (too many requests), status '16' = incorrect code, and status '17' = too many wrong attempts. Access error details from error.response.data in catch blocks. Refer to the Vonage Verify API Response Codes documentation for complete status code meanings and implement user-friendly error messages for each scenario.
What phone number format does Vonage Verify require?
Vonage Verify requires phone numbers in E.164 format: a plus sign (+) followed by country code and subscriber number without spaces or special characters (e.g., +14155552671 for a US number). Use the libphonenumber-js library for reliable parsing, validation, and formatting. The basic regex /^\d{10,15}$/ used in this guide is insufficient for production – implement proper validation to prevent errors and ensure successful OTP delivery across international numbers.
How can I prevent OTP brute force attacks in my Vonage implementation?
Prevent OTP brute force attacks by: 1) Implementing rate limiting with express-rate-limit on both request and verification endpoints, 2) Limiting verification attempts per request_id (Vonage returns status '17' after too many wrong codes), 3) Adding delays between failed attempts, 4) Monitoring for suspicious patterns (multiple requests from same IP), 5) Using HTTPS in production, and 6) Implementing CAPTCHA for repeated failed attempts. Store attempt counts in your persistent storage and enforce strict limits before calling Vonage APIs.
Why is my Vonage OTP verification failing with status code 6?
Status code 6 means the verification request was not found or has expired. Vonage verification codes expire after 5 minutes by default (configurable via API or dashboard). This error occurs when: 1) Users enter the code after expiration, 2) The request_id is invalid or was already used, or 3) The request was cancelled. Implement a "Request New Code" option, check expiry times in your storage before calling verify.check(), and provide clear user feedback about code expiration. Call vonage.verify.cancel() before issuing new codes for the same number.
How much does Vonage Verify API cost and are there free credits?
Vonage offers free credits when you sign up for a new account, allowing you to test the Verify API without initial costs. After free credits are exhausted, pricing varies by destination country and delivery method (SMS vs voice). Check your account balance and usage in the Vonage API Dashboard. Monitor your balance in production environments and implement alerts when credits run low to prevent service interruptions. Consider implementing fallback authentication methods if Vonage balance is depleted.
Frequently asked questions
Implement 2FA by integrating the Vonage Verify API into your Node.js Express app. This involves sending an OTP to the user's phone number via the API and then verifying the code entered by the user, adding an extra layer of security beyond just a password.
The Vonage Verify API is used for sending and verifying one-time passwords (OTPs) via SMS and voice calls, enabling two-factor authentication (2FA) in your Node.js applications. This enhances security by requiring users to have their phone to receive the code.
2FA enhances security in Node.js apps by requiring users to possess their phone to receive an OTP, adding a crucial layer of security beyond just a password. This makes it harder for attackers to gain access even if they have the user's password.
Redis is highly recommended for production OTP verification due to its speed and ability to handle temporary, expiring data. You store request IDs keyed by phone number with a TTL matching the OTP's expiry, enabling automatic deletion and scalability across multiple server instances.
Use the vonage.verify.start() method with the user's phone number, your brand name, and optional parameters like code length and workflow ID. This initiates the OTP process, sending an SMS message to the specified number.
You'll need Node.js and npm installed, a Vonage API account (free tier available), your Vonage API key and secret, and a basic understanding of Node.js, Express, and asynchronous JavaScript concepts like Promises and async/await.
Use the vonage.verify.check(requestId, code) method, providing the request ID obtained from vonage.verify.start() and the user-entered OTP. This checks if the entered code matches the one sent by Vonage.
Nunjucks is used as a templating engine to render dynamic HTML views in your Express application, similar to Jinja2 in Python. This allows you to easily create the user interface for entering phone numbers and OTP codes, displaying messages, and handling user interactions.
The .env file stores sensitive information, like your Vonage API key and secret, and is loaded using the dotenv module in your Node.js project. Importantly, it should never be committed to version control for security reasons.
Create a project directory, initialize a Node.js project with npm init -y, install required dependencies (express, @vonage/server-sdk, nunjucks, dotenv), structure your project with views, .env, .gitignore, index.js, and configure environment variables and Git ignore.
Implement try...catch blocks around Vonage API calls and check for specific Vonage error statuses and HTTP response codes to provide tailored error messages to the user. More robust logging should be implemented for production use.
No, in-memory storage for request IDs is unsuitable for production. Use Redis or a database for persistence and scalability across multiple servers, preventing data loss on restarts and ensuring data consistency.
Use robust input validation, implement rate limiting to prevent abuse, store secrets securely, enforce HTTPS, and follow secure session management practices if integrating with login workflows.
Check if the code has expired before verifying it using the Vonage API. Inform the user if their code is expired and provide a way to resend a new code. This is essential for a good user experience.
Add Two-Factor Authentication (2FA) via SMS One-Time Passwords (OTP) to your Node.js Express application using the Vonage Verify API. Build a simple application that requests an OTP for a user's phone number and verifies the code they enter. Use this guide as a starting point, demonstrating core concepts and production considerations.
Enhance your security by requiring users to possess their phone to receive a code – a critical layer beyond password authentication.
Project Goals:
Technologies Used:
.envfile.System Architecture:
Prerequisites:
1. Set Up Your Project
Initialize your Node.js project and install the necessary dependencies.
1. Create Project Directory:
Open your terminal and create a new directory for the project, then navigate into it:
mkdir vonage-otp-express cd vonage-otp-express2. Initialize Node.js Project:
Create a
package.jsonfile to manage dependencies and project metadata:3. Install Dependencies:
Install Express, the Vonage SDK, Nunjucks for templating, and
dotenvfor managing environment variables. Express includes built-in middleware for body parsing, sobody-parseris not needed as a separate dependency.4. Project Structure:
Create the basic file and directory structure:
views/: Contains HTML templates..env: Stores sensitive credentials (API Key, Secret). Never commit this file. Place in the project root directory..gitignore: Specifies files/directories Git should ignore (like.envandnode_modules).index.js: Main application file containing the Express server logic.package.json: Project configuration and dependencies.5. Configure Environment Variables (
.env):Create a file named
.envin the project root and add your Vonage API credentials..envReplace
YOUR_API_KEYandYOUR_API_SECRETwith the actual values from your Vonage Dashboard.6. Configure Git Ignore (
.gitignore):Create a
.gitignorefile to prevent committing sensitive information and unnecessary files:.gitignore7. Basic Express Server Setup (
index.js):Create the main application file
index.jsand set up the initial Express server, environment variables, middleware, and Nunjucks configuration.index.js// 1. Import dependencies require('dotenv').config(); // Load environment variables from .env file first (ensure it's in project root) const express = require('express'); const nunjucks = require('nunjucks'); const { Vonage } = require('@vonage/server-sdk'); // --- Basic Application Setup --- const app = express(); const port = process.env.PORT || 3000; // Use port from env or default to 3000 // --- Vonage Client Initialization --- // Check if API Key and Secret are set if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET) { console.error('❌ Missing Vonage API Key or Secret in .env file. Check your configuration.'); process.exit(1); // Exit if credentials are missing } const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET }); console.log('✅ Vonage client initialized.'); // --- Middleware Setup --- // Use built-in Express middleware for parsing JSON and URL-encoded bodies app.use(express.json()); // Parse JSON bodies (as sent by API clients) app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies (as sent by HTML forms) // --- Nunjucks Templating Engine Setup --- nunjucks.configure('views', { autoescape: true, // Enable automatic escaping to prevent XSS express: app // Link Nunjucks to the Express app }); app.set('view engine', 'html'); // Set default view engine extension console.log('✅ Middleware and Templating Engine configured.'); // --- Temporary Storage for Request IDs --- // IMPORTANT: This in-memory object is for demonstration purposes ONLY. // It is NOT suitable for production environments because: // 1. Data is lost when the server restarts. // 2. It does not scale across multiple server instances. // See Section 6 for production-ready storage (e.g., Redis, Database). const verificationRequests = {}; console.warn("⚠️ Using in-memory store for verification requests. This is NOT production-ready. Data will be lost on restart."); // --- Routes Will Go Here --- // GET / // POST /request-otp // POST /verify-otp // GET /success // GET /error // --- Start Server --- app.listen(port, () => { console.log(`🚀 Server listening on http://localhost:${port}`); }); // Basic error handling middleware (optional but recommended) app.use((err, req, res, next) => { console.error("❌ Global Error Handler:", err.stack); res.status(500).render('error.html', { errorMessage: 'An unexpected server error occurred.' }); });Explanation:
require('dotenv').config(): Loads variables from.envintoprocess.env. Ensure the.envfile is in the project root.express(),nunjucks: Standard setup.VonageInitialization: Creates the client instance using credentials from.env. Includes a check for credential existence.express.json()andexpress.urlencoded()are built-in Express middleware for parsing request bodies.extended: trueallows for rich objects and arrays to be encoded.autoescape: trueis crucial for security.verificationRequests: This simple in-memory object is for demonstration only. It maps phone numbers to their pendingrequest_id. It is explicitly not suitable for production. Section 6 discusses proper persistent storage solutions.app.listen: Starts the server.Run
node index.js. You should see console logs indicating successful initialization. Visitinghttp://localhost:3000will initially fail because routes aren't defined yet.2. Create Frontend Views
Create the simple HTML pages for user interaction. (Inline styles are used for simplicity; external CSS is recommended for larger applications).
1. Phone Number Input Form (
views/index.html):views/index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Enter Phone Number</title> <style> /* Basic styling for clarity */ body { font-family: sans-serif; padding: 20px; } label, input, button { display: block; margin-bottom: 10px; } input[type="tel"] { padding: 8px; width: 250px; } button { padding: 10px 15px; cursor: pointer; } .error { color: red; margin-bottom: 15px; } </style> </head> <body> <h1>Enter Phone Number for Verification</h1> {% if errorMessage %} <p class="error">{{ errorMessage }}</p> {% endif %} <form method="POST" action="/request-otp"> <label for="phoneNumber">Phone Number (with country code, e.g., 14155552671):</label> <input type="tel" id="phoneNumber" name="phoneNumber" required placeholder="14155552671"> <button type="submit">Send Verification Code</button> </form> </body> </html>2. OTP Code Input Form (
views/verify.html):views/verify.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Enter Verification Code</title> <style> /* Basic styling */ body { font-family: sans-serif; padding: 20px; } label, input, button { display: block; margin-bottom: 10px; } input[type="text"] { padding: 8px; width: 100px; } button { padding: 10px 15px; cursor: pointer; } .error { color: red; margin-bottom: 15px; } .info { color: grey; margin-bottom: 15px; font-size: 0.9em;} </style> </head> <body> <h1>Enter Verification Code</h1> <p class="info">A code was sent to your phone number (it may take a moment). Enter it below.</p> {% if errorMessage %} <p class="error">{{ errorMessage }}</p> {% endif %} <form method="POST" action="/verify-otp"> <label for="otpCode">Verification Code:</label> <input type="text" id="otpCode" name="otpCode" required pattern="\d{4,6}" title="Enter the 4 or 6 digit code"> <input type="hidden" name="requestId" value="{{ requestId }}"> <input type="hidden" name="phoneNumber" value="{{ phoneNumber }}"> <button type="submit">Verify Code</button> </form> </body> </html>3. Success Page (
views/success.html):views/success.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Verification Successful</title> <style> body { font-family: sans-serif; padding: 20px; color: green; } </style> </head> <body> <h1>Verification Successful!</h1> <p>Your phone number has been successfully verified.</p> <p><a href="/">Start Over</a></p> </body> </html>4. Error Page (
views/error.html):views/error.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Verification Error</title> <style> body { font-family: sans-serif; padding: 20px; color: red; } </style> </head> <body> <h1>Verification Failed</h1> <p>An error occurred:</p> <p><strong>{{ errorMessage | default('An unknown error occurred.') }}</strong></p> <p><a href="/">Try Again</a></p> </body> </html>3. Build the API Routes (Express Routes)
Implement the backend logic in
index.js.1. Root Route (
GET /):Renders the initial phone number input form.
Add this inside
index.jswhere// --- Routes Will Go Here ---is marked:// --- Routes --- // Render the initial page to enter phone number app.get('/', (req, res) => { res.render('index.html'); });2. Request OTP Route (
POST /request-otp):Handles phone number submission, calls Vonage to send the OTP, stores the
request_id(temporarily), and renders the verification page.Add this below the
GET /route inindex.js:// Handle the request to send an OTP app.post('/request-otp', async (req, res) => { const phoneNumber = req.body.phoneNumber; // Basic input validation if (!phoneNumber) { return res.status(400).render('index.html', { errorMessage: 'Phone number is required.' }); } // Basic format check (digits only, length 10 – 15). // WARNING: This regex is simplistic and NOT robust for validating international E.164 formats. // Use a library like 'libphonenumber-js' for proper validation in production (See Section 8). if (!/^\d{10,15}$/.test(phoneNumber)) { return res.status(400).render('index.html', { errorMessage: 'Invalid phone number format. Include country code without symbols (e.g., 14155552671).' }); } console.log(`📱 Requesting OTP for ${phoneNumber}...`); try { // Start the Vonage Verify request // Defaults: 4-digit code, 5-minute expiry. See optional params below. const response = await vonage.verify.start({ number: phoneNumber, brand: "MyAppVerification", // Customize your brand name shown in the SMS // code_length: '6', // Optional: Specify '6' for 6-digit code // workflow_id: 6 // Optional: Specify workflows (e.g., 6 for SMS only) // See Vonage docs for workflow IDs }); // Check if the request was successful based on Vonage response structure if (response.request_id) { const requestId = response.request_id; console.log(`✅ OTP Request successful. Request ID: ${requestId}`); // Store the request ID associated with the phone number (TEMPORARY IN-MEMORY STORAGE) verificationRequests[phoneNumber] = requestId; console.log("📝 Stored Request ID (In-Memory):", verificationRequests); // Render the page to enter the OTP code res.render('verify.html', { requestId: requestId, phoneNumber: phoneNumber }); } else { // This block might be reached if the Vonage API structure changes or returns unexpected success format console.error("⚠️ Unexpected Vonage response structure:", response); res.render('error.html', { errorMessage: 'Failed to initiate OTP request. Unexpected response.' }); } } catch (error) { console.error("❌ Error requesting OTP:", error); // Handle specific Vonage errors and other issues let userErrorMessage = 'An error occurred while sending the verification code.'; // Check both HTTP status from the error object and Vonage-specific status in the response data const httpStatus = error.response?.status; const vonageErrorData = error.response?.data; if (vonageErrorData) { console.error(`Vonage Error Details (HTTP Status: ${httpStatus}):`, vonageErrorData); // Example: Handle throttling or invalid number errors specifically if (vonageErrorData.status === '10') { // Throttled userErrorMessage = "Wait a moment before requesting another code for this number."; } else if (vonageErrorData.status === '3') { // Invalid number userErrorMessage = "The phone number provided is invalid. Check and try again."; } else { userErrorMessage = `Verification failed: ${vonageErrorData.error_text} (Status: ${vonageErrorData.status})`; } } else if (httpStatus) { userErrorMessage = `Failed to send code. Server responded with status ${httpStatus}.`; } // Else: Generic error message remains // Render the index page again with an error message res.status(httpStatus || 500).render('index.html', { errorMessage: userErrorMessage }); } });3. Verify OTP Route (
POST /verify-otp):Handles code submission, calls Vonage to check the code against the
request_id, and renders success or error pages.Add this below the
POST /request-otproute inindex.js:// Handle the OTP verification app.post('/verify-otp', async (req, res) => { const otpCode = req.body.otpCode; const requestId = req.body.requestId; const phoneNumber = req.body.phoneNumber; // Retrieve phone number // Basic validation if (!otpCode || !requestId || !phoneNumber) { return res.status(400).render('error.html', { errorMessage: 'Missing OTP code, request ID, or phone number.' }); } if (!/^\d{4,6}$/.test(otpCode)) { return res.status(400).render('verify.html', { errorMessage: 'Invalid OTP format. Enter the 4 or 6 digit code.', requestId: requestId, // Pass context back to the form phoneNumber: phoneNumber }); } console.log(`🔍 Verifying OTP ${otpCode} for Request ID: ${requestId}...`); try { // Check the OTP code with Vonage const result = await vonage.verify.check(requestId, otpCode); console.log("📋 Vonage Check Response:", result); // Check the status from the Vonage response if (result && result.status === '0') { // Status '0' means successful verification console.log(`✅ Verification successful for Request ID: ${requestId}`); // IMPORTANT: Clear the stored request ID after successful verification (from temporary store) if (verificationRequests[phoneNumber]) { delete verificationRequests[phoneNumber]; console.log("🗑️ Cleared Request ID for:", phoneNumber, "(In-Memory)"); } res.render('success.html'); } else { // Verification failed (wrong code, expired, etc.) const errorMessage = result.error_text || `Verification failed with status: ${result.status}`; console.warn(`⚠️ Verification failed for Request ID: ${requestId}. Reason: ${errorMessage}`); // Render the verification page again with an error res.status(400).render('verify.html', { errorMessage: `Verification Failed: ${errorMessage}`, requestId: requestId, // Pass context back to the form phoneNumber: phoneNumber }); } } catch (error) { console.error("❌ Error verifying OTP:", error); let userErrorMessage = 'An error occurred during verification.'; const httpStatus = error.response?.status; const vonageErrorData = error.response?.data; if (vonageErrorData) { console.error(`Vonage Error Details (HTTP Status: ${httpStatus}):`, vonageErrorData); // Example: Specific handling for error status codes from check endpoint if (vonageErrorData.status === '16') { // Incorrect code userErrorMessage = "The code you entered was incorrect. Try again."; } else if (vonageErrorData.status === '17') { // Wrong code too many times userErrorMessage = "You entered the wrong code too many times. Request a new code."; } else if (vonageErrorData.status === '6') { // Request not found / expired userErrorMessage = "Verification request expired or was not found. Request a new code."; } else { userErrorMessage = `Verification error: ${vonageErrorData.error_text} (Status: ${vonageErrorData.status})`; } // Render the verification page again with the error res.status(httpStatus || 500).render('verify.html', { errorMessage: userErrorMessage, requestId: requestId, // Pass context back phoneNumber: phoneNumber }); } else { // Render a generic error page if no specific Vonage error info res.status(httpStatus || 500).render('error.html', { errorMessage: userErrorMessage }); } } }); // Add placeholder routes for direct access to success/error (optional) app.get('/success', (req, res) => res.render('success.html')); app.get('/error', (req, res) => res.render('error.html', { errorMessage: 'An unspecified error occurred.'}));Explanation:
express-validator) is recommended for production. The phone regex is overly simple.vonage.verify.start: Initiates OTP.brandappears in SMS. Default code length (4) and expiry (5 minutes) are mentioned.request_idStorage: Temporarily stores the ID in the insecureverificationRequestsobject.vonage.verify.check: Verifies the code against the ID.try...catchblocks handle errors. Specific Vonage statuses (3,6,10,16,17) and HTTP status codes (error.response.status) are checked for better feedback.verificationRequestson success. Essential for the demo, but real storage needs proper management (TTL, etc.).4. Integrate Vonage Verify API (Recap and Details)
Key integration points:
.env.@vonage/server-sdkinitialized once.vonage.verify.start(options): Sends OTP. Returns{ request_id: '...' }on success.vonage.verify.check(requestId, code): Checks OTP. Returns object withstatus('0' = success).Environment Variables Summary:
VONAGE_API_KEY: Your Vonage API Key.VONAGE_API_SECRET: Your Vonage API Secret.PORT(Optional): Server port (defaults to3000).5. Error Handling and Logging
The basic error handling can be improved for production:
error.response.status(HTTP status code) in addition toerror.response.data.status(Vonage status) for more context.async-retry), especially forverify.check. Be cautious retryingverify.startdue to throttling.6. Production Considerations: Persistent Storage
The in-memory
verificationRequestsobject used in this guide is strictly for demonstration and NOT suitable for production.Why In-Memory Storage Fails in Production:
Production Solutions:
request_idkeyed by phone number (or vice-versa).VerificationRequeststable/collection.request_id,phoneNumber,createdAt,expires_at,status('pending', 'verified', etc.).request_idfor verification.expires_at.Conceptual Schema (SQL Example):
CREATE TABLE VerificationRequests ( request_id VARCHAR(50) PRIMARY KEY, -- Vonage Request ID phone_number VARCHAR(20) NOT NULL, status VARCHAR(10) NOT NULL DEFAULT 'pending', -- 'pending', 'verified', 'failed', 'expired' created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, -- Set based on Vonage expiry (e.g., NOW() + INTERVAL '5 minutes') attempts INT DEFAULT 0 ); CREATE INDEX idx_verification_phone ON VerificationRequests (phone_number); CREATE INDEX idx_verification_expires ON VerificationRequests (expires_at);Implementation: Replace
verificationRequests[key] = valueanddelete verificationRequests[key]with calls to your chosen persistent store (e.g., usingredis,pg,mongoose). Handle potential data store errors.7. Adding Security Features
Essential security practices:
express-validator,joi) for strict validation of phone numbers and OTP codes. Sanitize inputs.express-rate-limitor similar.request_id/IP..envor secrets.express-sessionwith a secure store, secure cookie attributes:HttpOnly,Secure,SameSite).8. Handling Special Cases
libphonenumber-jsfor reliable parsing, validation, and formatting on the server-side. Guide users in the UI.10means too many requests to the same number recently (~30 second window). Inform the user to wait. Check your persistent store before calling Vonage to see if a recent request for that number is still pending.expires_atin DB or rely on Redis TTL) before attemptingverify.check. Handle status6(not found/expired) gracefully.vonage.verify.startfor a resend, callvonage.verify.cancel(previous_request_id)if a previous request for the same number is still active within Vonage's system (typically within the expiry window). This prevents users from having multiple potentially valid codes active simultaneously. Retrieve theprevious_request_idfrom your persistent store.request_idwith the resend call.request_idand reset theexpires_attimestamp.workflow_idparameter inverify.startto customize delivery (e.g., SMS only, voice only).9. Implementing Performance Optimizations
request_id,phone_number,expires_at).async/awaitor Promises correctly.10. Adding Monitoring, Observability, and Analytics
Understand system health and performance:
/healthendpoint checking dependencies.11. Troubleshooting and Caveats
Common issues:
.envand ensuredotenvloads first.3) Use E.164. Validate robustly.10) Inform user to wait. Check pending requests before calling Vonage.6) Prompt user to request a new code. Check your storage logic.16) Allow retries (with limits).17) Prompt user to request a new code.verificationRequests = {}is only for demonstration and will fail in production or multi-instance environments. Use persistent storage (Section 6).12. Deployment and CI/CD
VONAGE_API_KEY,VONAGE_API_SECRET, and database/Redis credentials securely in the deployment environment. Do not commit secrets.PORTVariable: Useprocess.env.PORT.npm install --production.pm2or platform mechanisms (Procfile,Dockerfile CMD).Example
Dockerfile(Conceptual):13. Verification and Testing
Ensure correctness:
1. Manual Verification Steps:
node index.js.http://localhost:3000.2. API Testing (using
curlor Postman):Request OTP:
curl -X POST http://localhost:3000/request-otp \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "phoneNumber=YOUR_PHONE_NUMBER_HERE"(Replace
YOUR_PHONE_NUMBER_HEREwith a valid E.164 number)Verify OTP (requires
requestIdfrom previous step's output/logs andphoneNumber):curl -X POST http://localhost:3000/verify-otp \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "requestId=YOUR_REQUEST_ID&otpCode=YOUR_OTP_CODE&phoneNumber=YOUR_PHONE_NUMBER_HERE"(Replace placeholders with actual values)
3. Automated Testing:
Implement unit tests (Jest, Mocha) for individual functions/modules and integration tests (Supertest) to test API endpoints and workflows. Mock the Vonage API calls during testing.
14. Frequently Asked Questions (FAQ)
How do I implement Two-Factor Authentication with Vonage in Node.js?
Implement Two-Factor Authentication with Vonage by: 1) Installing the
@vonage/server-sdkpackage, 2) Initializing the Vonage client with your API credentials, 3) Callingvonage.verify.start()to send an OTP to the user's phone, 4) Storing the returnedrequest_id, and 5) Verifying the user-entered code withvonage.verify.check(). Use Express routes to handle the request and verification flows, and implement persistent storage (Redis or database) for production environments.What is the Vonage Verify API and how does it work?
The Vonage Verify API is a service that sends One-Time Passwords (OTPs) via SMS or voice call to verify user phone numbers. When you call
vonage.verify.start(), Vonage sends a code to the specified phone number and returns arequest_id. Users enter the received code, which you then verify by callingvonage.verify.check()with therequest_idand code. A status of '0' indicates successful verification, while non-zero statuses indicate errors like incorrect codes or expired requests.What storage should I use for Vonage request IDs in production?
Use Redis or a database (PostgreSQL, MongoDB) for storing Vonage request IDs in production – never in-memory storage. Redis is optimal for this use case because it supports automatic expiration (TTL) matching Vonage's 5-minute default expiry, provides fast lookups, and works across multiple server instances. Database storage requires implementing your own cleanup logic but integrates well with existing application data. Store the
request_id, phone number, creation timestamp, and expiry time.How do I handle Vonage Verify API error codes?
Handle Vonage Verify API errors by checking the
statusfield in responses: Status '0' = success, status '3' = invalid number, status '6' = expired/not found request, status '10' = throttled (too many requests), status '16' = incorrect code, and status '17' = too many wrong attempts. Access error details fromerror.response.datain catch blocks. Refer to the Vonage Verify API Response Codes documentation for complete status code meanings and implement user-friendly error messages for each scenario.What phone number format does Vonage Verify require?
Vonage Verify requires phone numbers in E.164 format: a plus sign (+) followed by country code and subscriber number without spaces or special characters (e.g., +14155552671 for a US number). Use the
libphonenumber-jslibrary for reliable parsing, validation, and formatting. The basic regex/^\d{10,15}$/used in this guide is insufficient for production – implement proper validation to prevent errors and ensure successful OTP delivery across international numbers.How can I prevent OTP brute force attacks in my Vonage implementation?
Prevent OTP brute force attacks by: 1) Implementing rate limiting with
express-rate-limiton both request and verification endpoints, 2) Limiting verification attempts perrequest_id(Vonage returns status '17' after too many wrong codes), 3) Adding delays between failed attempts, 4) Monitoring for suspicious patterns (multiple requests from same IP), 5) Using HTTPS in production, and 6) Implementing CAPTCHA for repeated failed attempts. Store attempt counts in your persistent storage and enforce strict limits before calling Vonage APIs.Why is my Vonage OTP verification failing with status code 6?
Status code 6 means the verification request was not found or has expired. Vonage verification codes expire after 5 minutes by default (configurable via API or dashboard). This error occurs when: 1) Users enter the code after expiration, 2) The
request_idis invalid or was already used, or 3) The request was cancelled. Implement a "Request New Code" option, check expiry times in your storage before callingverify.check(), and provide clear user feedback about code expiration. Callvonage.verify.cancel()before issuing new codes for the same number.How much does Vonage Verify API cost and are there free credits?
Vonage offers free credits when you sign up for a new account, allowing you to test the Verify API without initial costs. After free credits are exhausted, pricing varies by destination country and delivery method (SMS vs voice). Check your account balance and usage in the Vonage API Dashboard. Monitor your balance in production environments and implement alerts when credits run low to prevent service interruptions. Consider implementing fallback authentication methods if Vonage balance is depleted.