Two-factor authentication (2FA) adds a critical layer of security to user accounts, typically by requiring a password plus a code sent to a trusted device. One-time passwords (OTPs) sent via SMS are a common and effective method for implementing 2FA.
This guide provides a step-by-step walkthrough for building a functional OTP verification flow in a Node.js application using the Express framework and the MessageBird Verify API for sending and validating SMS OTPs. We will build a simple web application that asks for a user's phone number, sends a verification code via MessageBird, and then verifies the code entered by the user. While this guide provides the core logic, remember that making it truly production-ready requires implementing additional measures like robust error handling, logging, rate limiting, and session management, as discussed in later sections.
Project Goals:
- Build a Node.js Express application demonstrating SMS-based OTP verification.
- Securely integrate with the MessageBird Verify API.
- Implement a clear user flow for entering a phone number and verifying the OTP.
- Provide robust error handling and outline basic security considerations.
Technologies Used:
- Node.js: JavaScript runtime environment.
- Express: Minimalist web framework for Node.js.
- MessageBird Verify API: Service for sending and verifying OTPs via SMS or voice.
- MessageBird Node.js SDK: Simplifies interaction with the MessageBird API.
- Handlebars: Templating engine for rendering HTML views.
- dotenv: Module to load environment variables from a
.env
file. - body-parser: Middleware to parse incoming request bodies.
System Architecture:
The flow involves these components:
- User's Browser: Interacts with the Express application, submitting the phone number and OTP.
- Node.js/Express Server:
- Serves HTML pages (via Handlebars).
- Receives user input (phone number, OTP).
- Communicates with the MessageBird API via the SDK.
- Handles the logic for initiating and verifying OTPs.
- MessageBird Verify API:
- Generates a secure OTP.
- Sends the OTP via SMS to the user's phone number.
- Receives verification requests from the Express server and confirms if the provided OTP is correct for the specific verification attempt.
Prerequisites:
- Node.js and npm (or yarn) installed.
- A MessageBird account with a Live API Key.
- A text editor or IDE (like VS Code).
- Basic understanding of Node.js, Express, and asynchronous JavaScript.
1. Setting up the Project
Let's start by creating our project directory and initializing it.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir node-messagebird-otp cd node-messagebird-otp
-
Initialize npm: Initialize the project with npm. This creates a
package.json
file.npm init -y
The
-y
flag accepts the default settings. -
Install Dependencies: We need several packages: Express for the web server, Handlebars for templating, the MessageBird SDK,
dotenv
for environment variables, andbody-parser
to handle form submissions.npm install express express-handlebars messagebird dotenv body-parser
-
Create Project Structure: Set up a basic directory structure for clarity:
node-messagebird-otp/ ├── node_modules/ ├── views/ │ ├── layouts/ │ │ └── main.handlebars │ ├── step1.handlebars │ ├── step2.handlebars │ └── step3.handlebars ├── .env ├── .gitignore ├── index.js └── package.json
views/
: Contains Handlebars templates.views/layouts/
: Contains the main HTML structure template..env
: Stores sensitive information like API keys (will be created next)..gitignore
: Specifies files/directories Git should ignore (likenode_modules
and.env
).index.js
: The main application file.
-
Create
.gitignore
: Create a file named.gitignore
and add the following lines to prevent committing sensitive files and dependencies:# Dependencies node_modules/ # Environment variables .env # Log files *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Optional editor directories .idea/ .vscode/
2. Environment Configuration: MessageBird API Key
The application needs your MessageBird API key to authenticate requests. We'll use dotenv
to manage this securely.
-
Obtain MessageBird API Key:
- Log in to your MessageBird Dashboard.
- Navigate to the Developers section in the left-hand sidebar.
- Click on the API access (REST) tab.
- If you don't have a key, click ""Add access key"". Ensure you create a Live key, not a test key. The Verify API generally requires a Live key for full functionality, unlike some other APIs that might work partially with Test keys.
- Copy your Live API access key. Treat this key like a password – keep it secret!
-
Create
.env
File: In the root of your project directory (node-messagebird-otp/
), create a file named.env
. -
Add API Key to
.env
: Add your copied Live API key to the.env
file like this:# .env MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY_HERE
Important: Replace
YOUR_LIVE_API_KEY_HERE
with the actual key you copied from the MessageBird dashboard.- Purpose: The
MESSAGEBIRD_API_KEY
variable holds your secret key.dotenv
will load this intoprocess.env
so your application can access it without hardcoding it in the source code.
- Purpose: The
3. Building the Express Application
Now, let's set up the basic Express server and configure Handlebars.
-
Create
index.js
: Create the main application file,index.js
, in the project root. -
Initial Setup (
index.js
): Add the following code toindex.js
to require dependencies and initialize the Express app, MessageBird SDK, and Handlebars:// index.js // 1. Require Dependencies const express = require('express'); const exphbs = require('express-handlebars'); const bodyParser = require('body-parser'); const dotenv = require('dotenv'); // 2. Load Environment Variables dotenv.config(); // This loads the variables from .env into process.env // 3. Initialize MessageBird SDK // Ensure MESSAGEBIRD_API_KEY is set in your .env file if (!process.env.MESSAGEBIRD_API_KEY || process.env.MESSAGEBIRD_API_KEY === 'YOUR_LIVE_API_KEY_HERE') { console.error('Error: MESSAGEBIRD_API_KEY is not set or is still the placeholder in the .env file.'); process.exit(1); // Exit if the API key is missing or not replaced } const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY); // 4. Initialize Express App const app = express(); const port = process.env.PORT || 8080; // Use port from env or default to 8080 // 5. Configure Handlebars app.engine('handlebars', exphbs.engine({ // Use exphbs.engine for newer versions defaultLayout: 'main' })); app.set('view engine', 'handlebars'); // Specify the views directory (optional if it's named 'views') app.set('views', './views'); // 6. Configure Body Parser Middleware // This is needed to parse data from HTML forms app.use(bodyParser.urlencoded({ extended: true })); // 7. Define Routes (We will add these in the next section) // app.get('/', ...); // app.post('/step2', ...); // app.post('/step3', ...); // 8. Start the Server app.listen(port, () => { console.log(`Server listening on http://localhost:${port}`); });
- We require necessary modules.
dotenv.config()
loads the.env
file.- We initialize the MessageBird SDK, passing the API key from
process.env
. A check ensures the key exists and isn't the placeholder value. - We set up Express, define the port (allowing override via environment variable).
- Handlebars is configured as the view engine, specifying the default layout file.
body-parser
middleware is added to easily access form data viareq.body
.
-
Create Main Layout (
views/layouts/main.handlebars
): This file defines the basic HTML structure for all pages. Createviews/layouts/main.handlebars
with the following content:<!DOCTYPE html> <html lang=""en""> <head> <meta charset=""UTF-8""> <meta name=""viewport"" content=""width=device-width_ initial-scale=1.0""> <title>MessageBird OTP Verification</title> <style> body { font-family: sans-serif; padding: 20px; } h1 { color: #333; } p { color: #555; } form { margin-top: 15px; } input[type=""tel""], input[type=""text""] { padding: 8px; margin-right: 5px; border: 1px solid #ccc; border-radius: 4px; } input[type=""submit""] { padding: 8px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } input[type=""submit""]:hover { background-color: #0056b3; } .error { color: red; font-weight: bold; margin-bottom: 10px; } </style> </head> <body> <h1>MessageBird OTP Verification Example</h1> <hr> </body> </html>
- The
{{{body}}}
placeholder is where the content from other Handlebars views (step1
,step2
,step3
) will be rendered.
- The
4. Implementing the OTP Flow
Now we'll implement the core logic: requesting the phone number, sending the OTP, and verifying the OTP.
Step 1: Requesting the Phone Number
-
Create View (
views/step1.handlebars
): This view presents a form for the user to enter their phone number. Createviews/step1.handlebars
:<p class=""error""> </p> <p>Please enter your phone number (in international format, e.g., +14155552671) to receive a verification code:</p> <form method=""post"" action=""/step2""> <label for=""number"">Phone Number:</label> <input type=""tel"" id=""number"" name=""number"" placeholder=""+1234567890"" required /> <input type=""submit"" value=""Send Code"" /> </form>
{{#if error}}...{{/if}}
: Conditionally displays an error message if passed from the server.- The form
POST
s data to the/step2
route. - The input
name=""number""
makes the value accessible asreq.body.number
on the server. type=""tel""
hints to browsers (especially mobile) that this is a phone number field.
-
Create Route (
index.js
): Add a route inindex.js
(beforeapp.listen
) to render this view when the user visits the root URL (/
):// index.js - Add this route before app.listen() // Route to display the first step (enter phone number) app.get('/', (req, res) => { res.render('step1'); // Renders views/step1.handlebars });
Step 2: Sending the Verification Code
When the user submits their phone number, we need to call the MessageBird Verify API to send the OTP.
-
Create View (
views/step2.handlebars
): This view asks the user to enter the code they received. It also includes a hidden field to pass the verificationid
to the next step. Createviews/step2.handlebars
:<p class=""error""> </p> <p>We have sent a verification code to your phone!</p> <p>Please enter the 6-digit code below:</p> <form method=""post"" action=""/step3""> <input type=""hidden"" name=""id"" value=""> "" / <label for=""token"">Verification Code:</label> <input type=""text"" id=""token"" name=""token"" pattern=""\d{6}"" title=""Enter the 6-digit code"" required /> <input type=""submit"" value=""Verify Code"" /> </form>
name=""id""
andvalue=""{{id}}""
: Passes the verification ID received from MessageBird.name=""token""
: The input for the user-entered OTP.pattern=""\d{6}""
: Basic HTML5 validation for a 6-digit code.
-
Create Route (
index.js
): Add thePOST /step2
route inindex.js
to handle the form submission fromstep1
:// index.js - Add this route before app.listen() // Route to handle phone number submission and initiate verification app.post('/step2', (req, res) => { const number = req.body.number; // Basic validation (insufficient for production; consider libraries like 'google-libphonenumber' for robust E.164 validation - see Security section) if (!number || !/^\+[1-9]\d{1,14}$/.test(number)) { console.warn(`Invalid phone number format attempt: ${number}`); // Note: This regex is a basic check. Production apps should implement more robust E.164 validation // using libraries like google-libphonenumber (discussed further in the Security section) // to handle various international formats correctly. return res.render('step1', { error: 'Invalid phone number format. Please use international format (e.g., +14155552671).' }); } console.log(`Initiating verification for: ${number}`); // Call MessageBird Verify API messagebird.verify.create(number, { // originator: 'VerifyApp', // Sender ID (alpha/numeric, max 11 chars or phone number) // IMPORTANT: Originator ('From' field) requirements vary drastically by country. // Alphanumeric senders (like 'VerifyApp') may require pre-registration or may not work // in regions like the US/Canada. Using a purchased MessageBird virtual number is often more reliable. // Check MessageBird documentation for country-specific Sender ID rules before deploying. originator: process.env.MESSAGEBIRD_ORIGINATOR || 'MessageBird', // Use env var or default template: 'Your verification code is %token.', // %token is replaced by the actual OTP // type: 'sms', // Default is sms // tokenLength: 6, // Default is 6 // timeout: 30 // Default is 30 seconds }, (err, response) => { if (err) { // Handle API errors console.error('MessageBird Verify API error:', err); let errorMessage = 'Failed to send verification code. Please try again.'; // Provide more specific feedback if possible, checking safely if (err.errors && err.errors.length > 0 && err.errors[0].description) { errorMessage = err.errors[0].description; } else if (err.statusCode === 401) { errorMessage = 'Authentication failed. Please check your API key.' } return res.render('step1', { error: errorMessage }); } // Verification initiated successfully console.log('Verification response:', response); // Log the full response for debugging // Render step 2, passing the verification ID res.render('step2', { id: response.id // Pass the verification ID to the next view }); }); });
- We extract the
number
fromreq.body
. - Basic validation checks if the number looks like an international format. Production apps need more robust validation. A warning comment and prose highlight this limitation.
messagebird.verify.create(number, options, callback)
is called.number
: The user's phone number.options
:originator
: The sender ID displayed on the user's phone. Crucially, regulations vary. Alphanumeric IDs might require registration or fail in certain countries (e.g., US/Canada). Using a purchased MessageBird number is often safer. Consult MessageBird's country-specific rules. An environment variableMESSAGEBIRD_ORIGINATOR
is suggested for flexibility.template
: The SMS message body.%token
is mandatory and will be replaced by the generated OTP.timeout
(optional): How long the code is valid (default 30 seconds).
callback(err, response)
: Handles the asynchronous response.- If
err
, log the error and re-renderstep1
with an error message extracted safely fromerr.errors[0].description
or a generic message. - If successful (
response
), log the response and renderstep2
, crucially passingresponse.id
to the view. Thisid
uniquely identifies this verification attempt.
- If
- We extract the
Step 3: Verifying the Code
The final step is to take the id
and the user-entered token
and ask MessageBird if they match.
-
Create View (
views/step3.handlebars
): A simple success message. Createviews/step3.handlebars
:<h2>Verification Successful!</h2> <p>Your phone number has been successfully verified.</p> <p><a href="" />Start Over</a></p>
-
Create Route (
index.js
): Add thePOST /step3
route inindex.js
to handle the submission fromstep2
:// index.js - Add this route before app.listen() // Route to handle OTP submission and complete verification app.post('/step3', (req, res) => { const id = req.body.id; // Verification ID from hidden input const token = req.body.token; // OTP entered by the user if (!id || !token) { console.warn('Missing ID or Token in verification attempt.'); // Avoid revealing which one is missing to the user return res.render('step2', { error: 'Verification failed. Please try again.', id: id // Pass ID back if available }); } console.log(`Verifying code for ID: ${id}, Token: ${token}`); // Call MessageBird Verify API to verify the token messagebird.verify.verify(id, token, (err, response) => { if (err) { // Verification failed (invalid token, expired, etc.) console.error('MessageBird Verify token error:', err); let errorMessage = 'Verification failed. Please check the code and try again.'; // Provide specific API error if available, checking safely if (err.errors && err.errors.length > 0 && err.errors[0].description) { errorMessage = err.errors[0].description + ' Please try sending a new code.'; } // Re-render step 2 with error, passing the original ID back return res.render('step2', { error: errorMessage, id: id // Pass ID back so the form still works for retry (if applicable) }); } // Verification successful! console.log('Verification successful:', response); // IMPORTANT: In a real app, you would now update the user's session/state // to indicate successful verification before rendering success. // This example just shows the success page directly. res.render('step3'); }); });
- We get the
id
andtoken
fromreq.body
. Basic check added for their presence. messagebird.verify.verify(id, token, callback)
is called.id
: The unique ID from theverify.create
response.token
: The OTP code entered by the user.callback(err, response)
:- If
err
, verification failed (wrong code, expired, etc.). Log the error and re-renderstep2
with an error message (extracted safely) and the originalid
(so the hidden field remains populated). - If successful (
response
), the code was correct. Log success and render thestep3
success view. A comment highlights that real apps need session updates here.
- If
- We get the
5. Running the Application
Your basic OTP verification flow is complete!
-
Save Files: Ensure all files (
index.js
,.env
,.gitignore
, and all.handlebars
files in theviews
directories) are saved. Make sure you replaced the placeholder in.env
. -
Start the Server: Open your terminal in the
node-messagebird-otp
directory and run:node index.js
You should see the output:
Server listening on http://localhost:8080
. If you see an error about the API key, double-check your.env
file. -
Test: Open your web browser and navigate to
http://localhost:8080
.- Enter your phone number in international format (e.g.,
+14155551234
). - Click ""Send Code"".
- Check your phone for an SMS containing the code (check spam if needed, and ensure your originator is valid).
- Enter the code on the verification page.
- Click ""Verify Code"".
- You should see the ""Verification Successful!"" page. Try entering an incorrect code or waiting too long (over 30 seconds) to see the error handling.
- Enter your phone number in international format (e.g.,
6. Error Handling and Logging
Our current error handling is basic. Production applications require more robust strategies:
- Logging: We used
console.log
andconsole.error
. In production, use a dedicated logging library likeWinston
orPino
. Configure log levels (debug, info, warn, error) and output formats (JSON is good for log aggregation tools). Log critical events: verification initiation, API errors, successful verification, failed verification attempts. - MessageBird Errors: The
err
object from the MessageBird SDK often contains anerrors
array (e.g.,err.errors[0]
). Useerr.errors[0].code
for specific error types anderr.errors[0].description
(accessed safely, as shown) for user-friendly messages. Refer to MessageBird API documentation for error code meanings. - User Feedback: Provide clear, non-technical error messages to the user. Avoid exposing raw API errors directly. Guide the user on what to do next (e.g., ""Invalid code, please try again."", ""Code expired, please request a new one."").
- Retry Mechanisms: For transient network errors when calling MessageBird, you could implement a simple retry logic (e.g., retry once or twice with a short delay). However, for user input errors (invalid token), retrying the same verification request is usually handled by letting the user resubmit the form on the
/step2
page. If the code expires or too many attempts fail, guide the user back to/
to request a new code.
Example Logging Enhancement (Conceptual):
Note: The following snippet is conceptual. Implementing this requires choosing and configuring a specific logging library (e.g., Winston, Pino) and integrating it into your application.
// Conceptual example using a placeholder logger
// const logger = require('./logger'); // Assume a configured logger exists
// Inside verify.create callback error handling:
// logger.error({
// message: 'MessageBird Verify API error during creation',
// apiError: err, // Log the full error object for detailed debugging
// userNumber: number // Log relevant context (avoid logging sensitive data excessively)
// });
// return res.render('step1', { error: userFriendlyMessage });
// Inside verify.verify callback error handling:
// logger.warn({
// message: 'MessageBird token verification failed',
// verifyId: id,
// apiError: err
// });
// return res.render('step2', { error: userFriendlyMessage, id: id });
7. Security Considerations
While MessageBird handles OTP generation and security, consider these points in your application:
- Input Validation:
- Phone Numbers: The basic regex
^\+[1-9]\d{1,14}$
is a start but insufficient for production. Use libraries likegoogle-libphonenumber
(npm install google-libphonenumber
) for robust parsing and validation according to E.164 standards. Sanitize input to prevent potential (though less likely for phone numbers) injection issues. - OTP Token: Ensure the token format matches expectations (e.g., 6 digits).
body-parser
helps prevent basic payload manipulation, but always validate input types and lengths on the server.
- Phone Numbers: The basic regex
- Rate Limiting: Crucial to prevent abuse (repeatedly sending OTPs to a number, brute-forcing tokens). Implement rate limiting on:
- The
/step2
endpoint (requesting OTPs): Limit requests per phone number and/or IP address per time window (e.g., 1 request per minute, 5 requests per hour per number). - The
/step3
endpoint (verifying OTPs): Limit verification attempts per verification ID and/or IP address per time window (e.g., 5 attempts per 15 minutes per ID). - Use middleware like
express-rate-limit
.
- The
- API Key Security: Never commit your
.env
file or hardcode the API key. Use environment variables managed securely in your deployment environment. - HTTPS: Always use HTTPS in production to encrypt communication between the user's browser and your server.
- Session Management (Critical Gap in this Example): Crucially, this guide demonstrates the OTP sending and verification mechanism in isolation. A real-world login or 2FA flow requires proper session management (e.g., using
express-session
with a secure store). After successful OTP verification (Step 3), the application should typically update the user's server-side session state to grant access or mark the 2FA challenge as complete, rather than just showing a success page. The current example does not implement this critical session component. - Secure State Management: Passing the verification
id
via a hidden form field between steps is simple but relies on client-side state. For higher security, especially in complex flows, consider storing the verification attempt status (e.g., 'pending', 'verified') server-side, perhaps linked to the user's session or a short-lived database record, instead of solely relying on the client passing theid
back correctly. - Brute Force Protection: Rate limiting on
/step3
helps. You could also implement lockouts after too many failed attempts for a specific verification ID or user account (if integrated with user accounts).
Example Rate Limiting (Conceptual):
Note: The following snippet using express-rate-limit
is conceptual. You need to install the library (npm install express-rate-limit
) and integrate this middleware correctly into your routes.
// Conceptual example using express-rate-limit
// const rateLimit = require('express-rate-limit');
// // Limit OTP requests per phone number
// const otpRequestLimiter = rateLimit({
// windowMs: 15 * 60 * 1000, // 15 minutes
// max: 5, // Limit each number to 5 requests per windowMs
// message: 'Too many OTP requests from this number, please try again after 15 minutes.',
// keyGenerator: (req, res) => req.body.number, // Use phone number as key
// handler: (req, res, next, options) => {
// console.warn(`Rate limit exceeded for OTP request: ${req.body.number}`);
// res.status(options.statusCode).render('step1', { error: options.message });
// }
// });
// // Limit verification attempts per verification ID
// const otpVerifyLimiter = rateLimit({
// windowMs: 10 * 60 * 1000, // 10 minutes
// max: 10, // Limit each ID to 10 verification attempts per windowMs
// message: 'Too many verification attempts, please try again later or request a new code.',
// keyGenerator: (req, res) => req.body.id, // Use verification ID as key
// handler: (req, res, next, options) => {
// console.warn(`Rate limit exceeded for OTP verification: ${req.body.id}`);
// // Pass ID back so form still works potentially
// res.status(options.statusCode).render('step2', { error: options.message, id: req.body.id });
// }
// });
// app.post('/step2', otpRequestLimiter, (req, res) => { /* ... route logic ... */ });
// app.post('/step3', otpVerifyLimiter, (req, res) => { /* ... route logic ... */ });
8. Testing
- Manual Testing: Follow the steps in Section 5. Test edge cases:
- Invalid phone number formats (non-international, too short/long).
- Incorrect OTP codes.
- Expired OTP codes (wait > 30 seconds before submitting).
- Submitting the forms with empty fields.
- Rapidly requesting codes or attempting verification (if rate limiting is implemented).
- Automated Testing (Recommended for Production):
- Unit Tests: Test individual functions or modules in isolation (e.g., phone number validation logic). Mock the MessageBird SDK calls using libraries like
sinon
orjest.mock
. - Integration Tests: Test the interaction between different parts of your application, including routes. Use libraries like
supertest
to make HTTP requests to your running Express app and assert responses. You would still likely mock the actual MessageBird API calls to avoid sending real SMS messages and incurring costs during tests.
- Unit Tests: Test individual functions or modules in isolation (e.g., phone number validation logic). Mock the MessageBird SDK calls using libraries like
9. Deployment
-
Environment Variables: Ensure
MESSAGEBIRD_API_KEY
(and potentiallyMESSAGEBIRD_ORIGINATOR
) is set as an environment variable in your deployment platform (Heroku Config Vars, AWS Secrets Manager, Docker environment variables, etc.). Do not deploy your.env
file. -
Platform: Choose a hosting platform (Heroku, AWS EC2/ECS/Lambda, Google Cloud Run, DigitalOcean App Platform, etc.). Follow their specific Node.js deployment guides.
-
package.json
Scripts: Add astart
script to yourpackage.json
if you haven't already:// package.json ""scripts"": { ""start"": ""node index.js"", ""test"": ""echo \""Error: no test specified\"" && exit 1"" },
Most platforms use
npm start
to run your application. -
HTTPS: Configure HTTPS on your deployment platform or via a load balancer/proxy.
-
CI/CD (Continuous Integration/Continuous Deployment): Set up a pipeline (GitHub Actions, GitLab CI, Jenkins) to automatically test and deploy your application when changes are pushed to your repository.
10. Troubleshooting and Caveats
- Invalid API Key: Error messages like ""Authentication failed"" or similar usually point to an incorrect or missing
MESSAGEBIRD_API_KEY
. Double-check the key in your environment variables/.env
file and ensure it's a Live key and not the placeholder. - Invalid Phone Number: Errors like ""recipient is invalid"" mean the number format submitted to the API was incorrect. Ensure it's in international format (e.g.,
+1...
). Use robust validation (Section 7). - SMS Not Received:
- First, check the MessageBird Dashboard logs (often under 'Verify API Logs', 'SMS Logs', or similar) for detailed delivery statuses and error messages directly from carriers. This is the most common way to diagnose delivery issues.
- Verify the
originator
being used is valid for the destination country. Alphanumeric senders are restricted in some regions (like the US/Canada) and may require pre-registration. Try using a purchased MessageBird virtual number as the originator if allowed. Check MessageBird's documentation on sender IDs. - Ensure the destination phone has signal and isn't blocking messages from unknown senders or shortcodes.
- Check for account balance issues on MessageBird if using a paid feature.
- Token Verification Errors:
Verification code is incorrect
: User entered the wrong code.Verification has expired
: User took too long (default > 30s) to enter the code.Verification not found or has already been verified
: Theid
might be wrong, or the code was already used successfully.