Adding a layer of security beyond just passwords is crucial for modern web applications. Two-Factor Authentication (2FA), often implemented using One-Time Passwords (OTP) sent via SMS, provides a robust way to verify user identity by requiring both something they know (password) and something they have (their phone).
This guide provides a complete, step-by-step walkthrough for implementing SMS OTP verification in a Node.js application using the Express framework and the Vonage Verify API. We will build a simple application that prompts a user for their phone number, sends them an OTP via SMS, and allows them to verify the code.
Project Overview and Goals
What We'll Build: A Node.js/Express web application with three core pages:
- A form to input a phone number to initiate OTP verification.
- A form to enter the OTP code received via SMS.
- A success page upon correct code verification.
Problem Solved: This implementation secures user actions (like login, password reset, or sensitive transactions) by adding a second verification factor delivered via SMS, significantly reducing the risk of unauthorized access.
Technologies Used:
- Node.js: A JavaScript runtime for building the backend server.
- Express: A minimal and flexible Node.js web application framework for handling routing and requests.
- Vonage Verify API: A service specifically designed to handle OTP generation, delivery (SMS, voice), retries, code validation, and expiration, simplifying the 2FA implementation.
- EJS: A simple templating engine for rendering basic HTML views.
- dotenv: A module to load environment variables from a
.env
file. - libphonenumber-js: (Optional but Recommended) For parsing and validating phone numbers.
Architecture:
graph LR
A[User's Browser] -- 1. Enters Phone Number --> B(Node.js/Express App);
B -- 2. Calls Verify API --> C(Vonage Verify API);
C -- 3. Sends OTP --> D(User's Phone via SMS);
A -- 4. Enters OTP Code --> B;
B -- 5. Calls Check API --> C;
C -- 6. Returns Verification Result --> B;
B -- 7. Displays Success/Failure --> A;
Prerequisites:
- Node.js and npm (or yarn) installed on your machine.
- A Vonage API account. Sign up here if you don't have one (free credit is available for new accounts).
- Your Vonage API Key and API Secret, found on the Vonage API Dashboard.
Final Outcome: A functional web application demonstrating the core Vonage Verify OTP flow, ready to be integrated into a larger authentication system.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
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-express
-
Initialize Node.js Project: Create a
package.json
file.npm init -y
-
Install Dependencies: We need Express for the web server, the Vonage Server SDK for interacting with the Verify API, EJS for simple HTML templates,
dotenv
for managing API credentials securely, and optionallylibphonenumber-js
for robust phone number validation.npm install express @vonage/server-sdk dotenv ejs libphonenumber-js
express
: Web framework.@vonage/server-sdk
: Official Vonage SDK for Node.js.dotenv
: Loads environment variables from a.env
file.ejs
: Templating engine for views.libphonenumber-js
: (Recommended) For parsing, validating, and formatting phone numbers.
-
Create Project Structure: Set up a basic structure for our files.
mkdir views touch index.js .env .gitignore
views/
: Directory to store our EJS template files.index.js
: Main application file containing our server logic..env
: File to store sensitive API keys (will not be committed to Git)..gitignore
: Specifies files and directories Git should ignore.
-
Configure
.gitignore
: Prevent committing sensitive files and dependencies. Add the following to.gitignore
:# Dependencies node_modules # Environment Variables .env # Logs npm-debug.log* yarn-debug.log* yarn-error.log*
-
Configure Environment Variables (
.env
): You need your Vonage API Key and Secret. Find these on your Vonage API Dashboard. Add the following to your.env
file, replacing the placeholders with your actual credentials:# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET VONAGE_BRAND_NAME=MyApp # Name shown in the SMS message (max 11 chars)
VONAGE_API_KEY
: Your public Vonage API key.VONAGE_API_SECRET
: Your private Vonage API secret.VONAGE_BRAND_NAME
: The brand name associated with the verification request (appears in the SMS). Keep it short.
-
Basic Server Setup (
index.js
): Set up the initial Express server structure and load environment variables.// index.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); // Optional: const parsePhoneNumber = require('libphonenumber-js'); const app = express(); const port = process.env.PORT || 3000; // Use port from env or default to 3000 // Initialize Vonage SDK const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET }); // Middleware app.use(express.json()); // Parse JSON bodies (built-in) app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies (form data - built-in) app.set('view engine', 'ejs'); // Set EJS as the view engine app.set('views', './views'); // Specify the views directory // Routes will go here // Start the server app.listen(port, () => { console.log(`Server listening at http://localhost:${port}`); });
- We load
dotenv
first to ensure environment variables are available. - We initialize Express and the Vonage SDK using credentials from
.env
. - Built-in Express middleware (
express.json
,express.urlencoded
) is used to handle request bodies. - EJS is configured as the template engine.
- The server starts listening on the specified port.
- We load
2. Implementing Core Functionality
Let's create the user interface elements.
-
Create the Phone Number Input View (
views/request.ejs
): This simple form asks the user for their phone number.<!-- views/request.ejs --> <!DOCTYPE html> <html> <head> <title>Request OTP</title> <style> body { font-family: sans-serif; padding: 20px; } label, input, button { display: block; margin-bottom: 10px; } .error { color: red; margin-bottom: 10px; } </style> </head> <body> <h1>Enter Your Phone Number</h1> <% if (typeof error !== 'undefined' && error) { %> <p class=""error""><%= error %></p> <% } %> <form method=""post"" action=""/request-otp""> <label for=""number"">Phone Number (e.g., 14155552671):</label> <input type=""tel"" id=""number"" name=""number"" required placeholder=""Include country code""> <button type=""submit"">Send OTP</button> </form> </body> </html>
- Includes a basic form posting to
/request-otp
. - Displays an error message if one is passed from the server.
- Includes a basic form posting to
-
Create the OTP Input View (
views/verify.ejs
): This form allows the user to enter the code they received.<!-- views/verify.ejs --> <!DOCTYPE html> <html> <head> <title>Verify OTP</title> <style> body { font-family: sans-serif; padding: 20px; } label, input, button { display: block; margin-bottom: 10px; } .error { color: red; margin-bottom: 10px; } </style> </head> <body> <h1>Enter OTP Code</h1> <p>An OTP code has been sent to your phone.</p> <% if (typeof error !== 'undefined' && error) { %> <p class=""error""><%= error %></p> <% } %> <form method=""post"" action=""/verify-otp""> <input type=""hidden"" name=""requestId"" value=""<%= requestId %>""> <label for=""code"">OTP Code:</label> <input type=""text"" id=""code"" name=""code"" required pattern=""\d{4_6}"" title=""Enter the 4 or 6 digit code""> <button type=""submit"">Verify Code</button> </form> </body> </html>
- Includes a hidden input field to pass the
requestId
back to the server. - The code input uses a pattern to suggest 4-6 digits (Vonage default is 4, but can be configured).
- Includes a hidden input field to pass the
-
Create the Success View (
views/success.ejs
): A simple page shown after successful verification.<!-- views/success.ejs --> <!DOCTYPE html> <html> <head> <title>Verification Success</title> <style> body { font-family: sans-serif; padding: 20px; color: green; } </style> </head> <body> <h1>Verification Successful!</h1> <p>Your phone number has been verified.</p> <p><a href="" />Start Over</a></p> </body> </html>
3. Building the API Layer
Now we'll implement the routes and logic for requesting and verifying the OTP.
-
Create the Root Route (
GET /
): This route simply renders the phone number input form. Add this toindex.js
before theapp.listen
call.// index.js (add this route) app.get('/', (req, res) => { res.render('request', { error: null }); // Render request.ejs, initially no error });
-
Implement the OTP Request Route (
POST /request-otp
): This route handles the phone number submission, calls the Vonage Verify API to start the verification process, and renders the OTP input form upon success.// index.js (add this route) app.post('/request-otp', async (req, res) => { let phoneNumber = req.body.number; const brand = process.env.VONAGE_BRAND_NAME; // Basic validation (enhance in production) if (!phoneNumber) { return res.render('request', { error: 'Phone number is required.' }); } // IMPORTANT: Add E.164 validation/normalization here in production! // Example using libphonenumber-js (conceptual): // try { // const parsedNumber = parsePhoneNumber(phoneNumber, 'US'); // Default country if needed // if (parsedNumber && parsedNumber.isValid()) { // phoneNumber = parsedNumber.number; // Use E.164 format // } else { // return res.render('request', { error: 'Invalid phone number format.' }); // } // } catch (e) { // return res.render('request', { error: 'Invalid phone number format.' }); // } // Ensure the phone number is in E.164 format (e.g., 14155552671) for best results with Vonage. console.log(`Requesting OTP for ${phoneNumber} with brand ${brand}`); try { const result = await vonage.verify.start({ number: phoneNumber, brand: brand, // workflow_id: 6 // Optional: Specify workflow (e.g., 6 for 6-digit code) // code_length: 6 // Optional: Specify code length (4 or 6) }); console.log('Vonage Verify Start Result:', result); if (result.status === '0') { // Successfully initiated verification const requestId = result.request_id; res.render('verify', { requestId: requestId, error: null }); // Render verify.ejs } else { // Handle Vonage API errors console.error('Vonage Verify Start Error:', result.error_text); res.render('request', { error: `Error starting verification: ${result.error_text}` }); } } catch (error) { // Handle network or other unexpected errors console.error('Error calling Vonage Verify API:', error); res.render('request', { error: 'An unexpected error occurred. Please try again.' }); } });
- We retrieve the phone number from the form data (
req.body.number
). - Crucially, in a production application, you must add robust validation and normalization here to ensure the
phoneNumber
is in E.164 format (e.g.,14155552671
) before sending it to Vonage. Usinglibphonenumber-js
is recommended (see commented example). - We call
vonage.verify.start()
with the (ideally validated) phone number and brand name. - The
vonage.verify.start
method handles sending the SMS (and potential voice fallbacks depending on the workflow). - If
result.status
is'0'
, the request was successful. We extract therequest_id
(crucial for the next step) and render theverify.ejs
view. - If
result.status
is not'0'
, we display theerror_text
from Vonage. - A
try...catch
block handles potential network errors when calling the Vonage API.
- We retrieve the phone number from the form data (
-
Implement the OTP Verification Route (
POST /verify-otp
): This route takes therequestId
and the user-enteredcode
, calls the Vonage Verify API to check the code, and renders the success or failure view.// index.js (add this route) app.post('/verify-otp', async (req, res) => { const requestId = req.body.requestId; const code = req.body.code; // Basic validation if (!requestId || !code) { // This shouldn't happen with the hidden field, but good practice return res.render('request', { error: 'Missing request ID or code.' }); } console.log(`Verifying OTP for request ID ${requestId} with code ${code}`); try { const result = await vonage.verify.check(requestId, code); console.log('Vonage Verify Check Result:', result); if (result.status === '0') { // Verification successful res.render('success'); // Render success.ejs } else { // Verification failed (e.g., wrong code, expired) console.error('Vonage Verify Check Error:', result.error_text); // Render the verify page again with an error message res.render('verify', { requestId: requestId, // Pass requestId back to the view error: `Verification failed: ${result.error_text}. Please try again.` }); } } catch (error) { // Handle network or other unexpected errors console.error('Error calling Vonage Check API:', error); res.render('verify', { requestId: requestId, // Pass requestId back error: 'An unexpected error occurred during verification. Please try again.' }); } });
- Retrieves
requestId
andcode
from the form data. - Calls
vonage.verify.check()
with these two parameters. - If
result.status
is'0'
, the code is correct, render the success page. - If not
'0'
, the code is incorrect, expired, or another issue occurred. Render theverify.ejs
view again, passing back therequestId
and displaying theerror_text
. - Again, a
try...catch
handles potential network errors.
- Retrieves
4. Integrating with Vonage
Integration primarily involves:
- Account Setup: Signing up for a Vonage account.
- API Credentials: Obtaining your API Key and Secret from the Vonage API Dashboard.
- Navigate to the dashboard after logging in.
- The API Key and Secret are usually displayed prominently near the top.
- Secure Storage: Storing these credentials securely using environment variables (
.env
file) as demonstrated in the setup section. Never commit your.env
file or hardcode secrets directly in your source code. - SDK Initialization: Using the
@vonage/server-sdk
and initializing it with the credentials from environment variables:// index.js (Initialization part) const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET });
- API Calls: Using the SDK methods (
verify.start
,verify.check
) as shown in the route implementations.
Environment Variables Explained:
VONAGE_API_KEY
: (String) Your public Vonage API identifier. Required for authentication. Obtain from Vonage Dashboard.VONAGE_API_SECRET
: (String) Your private Vonage API secret key. Required for authentication. Obtain from Vonage Dashboard. Keep this highly confidential.VONAGE_BRAND_NAME
: (String, Max 11 Alphanumeric Chars) The name displayed as the sender in the SMS message (actual sender ID may vary by country/carrier). Define this in your.env
.
5. Implementing Error Handling and Logging
Robust error handling and logging are essential for production.
-
Consistent Error Handling Strategy:
- Use
try...catch
blocks around all external API calls (Vonage SDK calls). - Check the
result.status
from Vonage API responses. A status of'0'
indicates success. Any other status indicates an error. - Use the
result.error_text
provided by Vonage for specific error details. - Render user-friendly error messages on the frontend (as shown in the
.ejs
templates), passing specific error details when appropriate but avoiding exposure of sensitive system information. - For unexpected errors (network issues, code bugs), provide a generic error message to the user while logging detailed information on the server.
- Use
-
Logging:
- Use
console.log
for basic informational messages during development (e.g., "Requesting OTP for...", "Verifying OTP..."). - Use
console.error
for logging actual errors, including the error object or Vonageerror_text
. - Production Logging: For production, replace
console.log
/console.error
with a dedicated logging library like Winston or Pino. Configure appropriate log levels (e.g., INFO, WARN, ERROR) and output formats (e.g., JSON) and direct logs to persistent storage or a log management service.
// Example logging within a catch block catch (error) { console.error({ message: 'Error calling Vonage Check API', requestId: requestId, // Log relevant context error: error.message, // Log the error message stack: error.stack // Log stack trace for debugging }); res.render('verify', { /* ... render error view ... */ }); }
- Use
-
Retry Mechanisms:
- Vonage Internal Retries: The Vonage Verify API itself handles retries for delivering the OTP (e.g., SMS -> Voice Call -> Voice Call) according to the chosen workflow (default is workflow 1). You generally don't need to implement application-level retries for code delivery.
- API Call Retries: For transient network errors when calling the Vonage API (
verify.start
orverify.check
), you could implement a simple retry mechanism with exponential backoff using libraries likeasync-retry
. However, for this basic OTP flow, failing the request and asking the user to try again might be acceptable.
6. Database Schema and Data Layer (Integration Context)
While this standalone example doesn't require a database, integrating OTP verification into a real application typically involves linking the verification status to a user account.
-
Concept: After a successful OTP check (
vonage.verify.check
returns status'0'
), you would update a flag in your user database record to indicate that the phone number associated with that user has been verified. -
Example Schema (Conceptual - using Prisma syntax):
// Example schema.prisma (Conceptual) model User { id String @id @default(cuid()) email String @unique passwordHash String phoneNumber String? @unique // Store phone number used for OTP (E.164 format) isPhoneVerified Boolean @default(false) // Flag set after successful OTP check createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
-
Workflow:
- User initiates an action requiring phone verification (e.g., sign-up, profile update).
- Store the user's phone number (in E.164 format) in the
User
record. - Initiate the Vonage Verify flow (
/request-otp
). - User submits the code (
/verify-otp
). - If
vonage.verify.check
is successful:- Find the user record associated with the phone number (or the logged-in user session).
- Update
isPhoneVerified
totrue
. - Persist the change to the database.
- Proceed with the user's original action (e.g., complete sign-up, allow profile change).
-
Data Access: Use an ORM like Prisma or Sequelize to manage database interactions.
-
Migrations: Use the migration tools provided by your ORM (e.g.,
prisma migrate dev
) to manage schema changes.
7. Adding Security Features
Security is paramount for authentication flows.
-
Input Validation and Sanitization:
- Phone Number: Validate the format rigorously before sending to Vonage. Use a library like
libphonenumber-js
to parse, validate, and normalize phone numbers to E.164 format. This is crucial to prevent errors and potential abuse.// Conceptual Example in /request-otp route using libphonenumber-js const parsePhoneNumber = require('libphonenumber-js'); // ... inside the route handler ... let phoneNumberInput = req.body.number; let phoneNumberE164; try { const parsedNumber = parsePhoneNumber(phoneNumberInput); // Can add default country e.g., 'US' if (parsedNumber && parsedNumber.isValid()) { phoneNumberE164 = parsedNumber.number; // Get E.164 format (e.g., +14155552671) console.log(`Validated number: ${phoneNumberE164}`); } else { console.warn(`Invalid phone number input: ${phoneNumberInput}`); return res.render('request', { error: 'Invalid phone number format.' }); } } catch (e) { console.error(`Error parsing phone number ${phoneNumberInput}:`, e); return res.render('request', { error: 'Invalid phone number format.' }); } // Now use phoneNumberE164 when calling vonage.verify.start // const result = await vonage.verify.start({ number: phoneNumberE164, brand }); // ... rest of the route ...
- OTP Code: Ensure the code consists only of digits and matches the expected length (typically 4 or 6). Reject any non-numeric input immediately on the client-side (using
pattern
) and server-side.
- Phone Number: Validate the format rigorously before sending to Vonage. Use a library like
-
Rate Limiting: Crucial to prevent abuse and control costs.
- Limit OTP Requests: Prevent users (identified by IP address or potentially phone number prefix after validation) from requesting too many OTP codes in a short period. This mitigates SMS pumping fraud and annoyance.
- Limit Verification Attempts: Prevent brute-force attacks on the OTP code itself. Limit the number of check attempts per
requestId
or IP address. Vonage has some built-in limits, but application-level limiting adds an extra layer. - Implementation: Use middleware like
express-rate-limit
.
// Example using express-rate-limit const rateLimit = require('express-rate-limit'); const otpRequestLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Limit each IP to 5 OTP requests per windowMs message: 'Too many OTP requests from this IP, please try again after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); const verifyAttemptLimiter = rateLimit({ windowMs: 5 * 60 * 1000, // 5 minutes max: 5, // Limit each IP to 5 verification attempts per windowMs (adjust as needed) message: 'Too many verification attempts, please try again later.', standardHeaders: true, legacyHeaders: false, }); // Apply to routes in index.js BEFORE the route handlers app.post('/request-otp', otpRequestLimiter, async (req, res) => { /* ... */ }); app.post('/verify-otp', verifyAttemptLimiter, async (req, res) => { /* ... */ });
-
HTTPS: Always use HTTPS in production to encrypt communication between the client and server. Configure your hosting environment or reverse proxy (like Nginx) accordingly.
-
Secrets Management: Reiterate: Use environment variables for API keys and secrets. Do not hardcode them. Use your deployment platform's secrets management features.
-
Session Management (if integrated): If used within a login flow, ensure secure session handling practices (e.g., using
express-session
with a secure store and appropriate cookie settings).
8. Handling Special Cases
Real-world scenarios often involve edge cases.
- Phone Number Formatting: Always strive to convert numbers to E.164 format (
+
followed by country code and number, e.g.,+14155552671
) before sending to Vonage. The SDK might handle some variations, but explicit formatting using libraries likelibphonenumber-js
(as shown in Section 7) is much safer and more reliable. - Code Expiration: Vonage Verify codes expire (default 5 minutes, configurable). If a user tries to verify an expired code, the
vonage.verify.check
call will fail with an appropriate error status/text (e.g., status '16', error_text 'The code provided does not match the expected value' or status '17' for request not found). Inform the user clearly that the code has expired and they may need to request a new one. - Cancellation: A user might want to cancel an ongoing verification request (e.g., they entered the wrong number). You can implement this using
vonage.verify.cancel(requestId)
. Provide a ""Cancel"" or ""Resend Code"" button in the UI that triggers a route calling this method. Note that callingverify.start
again for the same number usually implicitly cancels the previous request. - Multiple Requests: If a user requests a code multiple times for the same number before verifying, Vonage handles this by invalidating previous codes when a new one is sent for the same
requestId
. Ensure your UI guides the user to use the latest code received. - SMS Delivery Delays: SMS is not instantaneous. Inform users that the code might take a minute or two to arrive. Vonage's fallback to voice calls (depending on workflow) helps mitigate delivery failures.
- International Numbers: Vonage Verify supports global numbers, but ensure your UI correctly captures country codes (or use
libphonenumber-js
to help parse) and that your Vonage account has permissions/funds for international SMS if needed. E.164 formatting is essential here.
9. Implementing Performance Optimizations
For a standard OTP flow, performance bottlenecks are less common unless handling extremely high volume.
- Asynchronous Operations: Node.js is naturally asynchronous. Ensure all I/O operations (like Vonage API calls, database interactions) use
async/await
or Promises correctly to avoid blocking the event loop. The provided code examples useasync/await
. - Efficient Database Queries: If integrating with a database (Section 6), ensure queries to find users or update verification status are properly indexed (e.g., on
phoneNumber
oruserId
). - Minimize Payload: Keep request/response payloads minimal.
- Caching: Caching is generally not applicable or recommended for the OTP codes or verification status itself due to their short-lived and sensitive nature. Caching user session data might be relevant if integrating into a login flow.
- Load Testing: For high-traffic applications, use tools like k6, Artillery, or JMeter to simulate load and identify potential bottlenecks in your routes or dependencies (especially rate limiting).
10. Adding Monitoring, Observability, and Analytics
Gain insights into how your OTP flow is performing.
- Logging: (Covered in Section 5) Ensure comprehensive logging of requests, successes, failures (with reasons), and errors using a structured logger in production.
- Metrics: Track key performance indicators (KPIs):
- Number of OTP requests (
/request-otp
calls). - Number of verification attempts (
/verify-otp
calls). - Verification success rate (successful checks / total checks).
- Verification failure rate (broken down by error type, e.g., 'wrong code', 'expired', 'throttled', 'invalid number').
- Latency of Vonage API calls (
verify.start
,verify.check
). - Rate limit triggers.
- Implementation: Use libraries like
prom-client
for Prometheus metrics or integrate with Application Performance Monitoring (APM) tools (Datadog, New Relic, Dynatrace).
- Number of OTP requests (
- Health Checks: Implement a simple
/health
endpoint that returns a200 OK
status if the server is running and basic checks pass (e.g., can initialize Vonage SDK).// index.js (add health check route) app.get('/health', (req, res) => { // Add more sophisticated checks if needed (e.g., DB connection) res.status(200).send('OK'); });
- Error Tracking: Integrate services like Sentry, Bugsnag, or APM tools to automatically capture and report unhandled exceptions and errors in real-time, providing context like request IDs.
- Dashboards: Create dashboards (e.g., in Grafana, Kibana, or your APM tool) visualizing the key metrics defined above to monitor the health, performance, and potential abuse patterns of the OTP system.
11. Troubleshooting and Caveats
Common issues and their solutions:
- Error:
Authentication failed
/ Status1
or2
(from Start/Check): Double-checkVONAGE_API_KEY
andVONAGE_API_SECRET
in your environment variables. Ensure they are correct, have no extra spaces, and are being loaded properly bydotenv
or your platform's secret management. - Error:
Invalid number
/ Status3
(from Start): The phone number format is likely incorrect or invalid as perceived by Vonage. Ensure it's normalized to E.164 format (14155552671
- Vonage often prefers without the+
) and is a valid, reachable number. Uselibphonenumber-js
for validation before calling Vonage. Check Vonage dashboard logs for the exact number sent. - Error:
Your account does not have sufficient credit
/ Status9
(from Start): Your Vonage account balance is too low. Add funds via the Vonage dashboard. - Error:
Throttled
/ Status6
(from Start) or1
(from Check): You've hit Vonage's API rate limits or internal velocity checks. Implement application-level rate limiting (Section 7) and ensure it's working. If legitimate traffic is throttled, analyze patterns and potentially contact Vonage support. - Error:
The code provided does not match the expected value
/ Status16
(from Check): The user entered the wrong OTP code. Render the verify form again with an error message. - Error:
Request '...' was not found or it has been verified already
/ Status17
(from Check): TherequestId
is invalid, has expired (default 5 mins), or was already successfully verified. This can happen if the user takes too long, tries to reuse an old link/request, or double-submits the form. Handle by prompting the user to start the process over. - SMS Not Received:
- Verify the phone number is correct (E.164 format) and reachable. Log the number being sent.
- Check carrier filtering (less common with Vonage Verify's shared shortcodes/numbers, but possible).
- Check Vonage account settings for any country restrictions.
- Inform the user about potential delays and consider UI options like a "Resend Code" button (which triggers
/request-otp
again).