This guide provides a step-by-step walkthrough for integrating Plivo's Verify API into a Next.js application to implement robust One-Time Password (OTP) verification, often used for Two-Factor Authentication (2FA) or phone number validation during sign-up.
We will build a simple Next.js application featuring:
- A UI to input a phone number.
- An API endpoint to trigger an OTP request via Plivo Verify (using SMS).
- A UI to input the received OTP.
- An API endpoint to validate the OTP using Plivo Verify.
- Basic state management, error handling, and security considerations.
This implementation solves the critical need for verifying user phone numbers, enhancing security against fake accounts and unauthorized access, while leveraging Plivo's global reach, high deliverability, and built-in fraud protection.
Technologies Used:
- Next.js: A React framework for building server-side rendered and statically generated web applications. Chosen for its developer experience, performance features, and integrated API routes.
- Plivo Verify API: A service for sending and validating OTPs via SMS, Voice, and WhatsApp. Chosen for its simple API, competitive pricing, global coverage, and features like Fraud Shield.
- Node.js: The runtime environment for Next.js and the Plivo SDK.
- React: For building the user interface components.
- Tailwind CSS (Optional): For styling the UI (examples will use basic HTML/CSS for clarity, but Tailwind is common in Next.js).
Prerequisites:
- Node.js (v18 or later recommended) and npm/yarn installed.
- A Plivo account (Sign up at plivo.com).
- Basic understanding of React and Next.js concepts (components, hooks, API routes).
- Access to a terminal or command prompt.
System Architecture:
The typical flow involves the user's browser interacting with Next.js API routes, which in turn communicate with the Plivo Verify API to send an OTP via SMS to the user's phone and later validate the OTP entered by the user.
Final Outcome:
By the end of this guide, you will have a functional Next.js page where a user can enter their phone number, receive an OTP via SMS managed by Plivo, enter that OTP, and have it validated, with UI feedback indicating success or failure. You will also understand how to securely handle API credentials, implement basic error handling, and consider security measures like rate limiting.
1. Setting Up the Project
Let's start by creating a new Next.js project and installing the necessary dependencies.
-
Create a Next.js App: Open your terminal and run the following command. Replace
plivo-nextjs-otp
with your preferred project name. Follow the prompts (we recommend using TypeScript and App Router if prompted, but this guide uses Pages Router for simplicity in API routes).npx create-next-app@latest plivo-nextjs-otp # Follow the prompts (e.g., choose Pages Router for this guide) cd plivo-nextjs-otp
-
Install Plivo Node.js SDK: This package provides convenient methods for interacting with the Plivo API.
npm install plivo # or yarn add plivo
-
Set Up Environment Variables: Sensitive information like API keys should never be hardcoded. We'll use environment variables.
-
Create a file named
.env.local
in the root of your project. -
Add the following lines, replacing the placeholder values later:
# Plivo Credentials - Get from Plivo Console > API > Keys & Credentials PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
-
Important: Add
.env.local
to your.gitignore
file (it should be there by default in Next.js projects) to prevent accidentally committing your secrets.
Why
.env.local
? Next.js automatically loads variables from this file for local development. For production deployment, you'll need to configure these environment variables directly in your hosting provider's settings (e.g., Vercel, Netlify). -
-
Project Structure: Your basic structure (using Pages Router) will look like this:
plivo-nextjs-otp/ ├── .env.local ├── .gitignore ├── node_modules/ ├── package.json ├── pages/ │ ├── api/ # API Routes live here │ │ └── otp/ │ │ ├── send.js │ │ └── verify.js │ ├── _app.js │ └── index.js # Our main page component ├── public/ └── styles/
-
Run the Development Server: Verify the basic setup is working.
npm run dev # or yarn dev
Open your browser to
http://localhost:3000
. You should see the default Next.js welcome page.
2. Implementing Core Functionality (API Layer)
Now, we'll build the backend API routes within Next.js to handle sending and verifying OTPs using the Plivo SDK.
/api/otp/send
)
2.1 API Route to Send OTP (This endpoint will receive a phone number, initiate the Plivo Verify session, and return the session_uuid
to the client.
-
Create the file:
pages/api/otp/send.js
-
Add the following code:
// pages/api/otp/send.js import { Client } from 'plivo'; // Ensure environment variables are loaded (Next.js handles this automatically) const authId = process.env.PLIVO_AUTH_ID; const authToken = process.env.PLIVO_AUTH_TOKEN; if (!authId || !authToken) { console.error(""Plivo Auth ID or Auth Token is missing in environment variables.""); // In a real app, you might throw an error or handle this more gracefully } const client = new Client(authId, authToken); export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method Not Allowed' }); } const { recipient } = req.body; // Basic validation - enhance as needed (e.g., regex for phone format) if (!recipient) { return res.status(400).json({ message: 'Recipient phone number is required' }); } console.log(`Sending OTP request to: ${recipient}`); try { const response = await client.verify.session.create({ recipient: recipient, channel: 'sms', // Specify channel (sms, voice, whatsapp) // Add other parameters like 'locale', 'url' for callbacks if needed // See Plivo Verify docs: https://www.plivo.com/docs/verify/api/session#create-a-session }); console.log(""Plivo Create Session Response:"", response); // Return the session UUID to the client for the verification step res.status(200).json({ session_uuid: response.sessionUuid }); } catch (error) { console.error('Plivo API Error creating session:', error); // Provide a generic error message to the client // Log the specific error details on the server let errorMessage = 'Failed to send OTP. Please try again later.'; let statusCode = 500; // Check for specific Plivo error structures if available from the SDK // Example (structure might vary based on SDK/API version): if (error.message) { // You might parse error.message for specific Plivo error codes/reasons // e.g., invalid number format, insufficient balance, etc. // For now, we log the raw error server-side console.error(""Detailed Plivo Error:"", error.message); } // Based on Plivo error codes, you might set different status codes (e.g., 400 for bad input) // if (error.statusCode === 400) { // statusCode = 400; // errorMessage = 'Invalid phone number format provided.'; // } res.status(statusCode).json({ message: errorMessage }); } }
Explanation:
- We import the Plivo
Client
. - We initialize the client using credentials from environment variables. Crucially, we check if they exist.
- The handler only accepts
POST
requests. - We extract the
recipient
phone number from the request body. Basic validation is included. client.verify.session.create()
is called with the recipient number andchannel: 'sms'
. This tells Plivo to generate an OTP and send it via SMS.- On success, Plivo returns a response containing the
sessionUuid
. We extract and send this back to the client in the JSON response. - Error handling uses a
try...catch
block. We log the detailed error server-side and return a generic error message to the client.
- We import the Plivo
/api/otp/verify
)
2.2 API Route to Verify OTP (This endpoint receives the session_uuid
(obtained from the previous step) and the otp
entered by the user, then validates them with Plivo.
-
Create the file:
pages/api/otp/verify.js
-
Add the following code:
// pages/api/otp/verify.js import { Client } from 'plivo'; const authId = process.env.PLIVO_AUTH_ID; const authToken = process.env.PLIVO_AUTH_TOKEN; if (!authId || !authToken) { console.error(""Plivo Auth ID or Auth Token is missing in environment variables.""); } const client = new Client(authId, authToken); export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method Not Allowed' }); } const { session_uuid, otp } = req.body; if (!session_uuid || !otp) { return res.status(400).json({ message: 'Session UUID and OTP are required' }); } // *** OTP Length Assumption *** // This guide assumes a 6-digit OTP. Plivo might be configured differently. // Adjust the regex below and the `maxLength` attribute in the frontend UI // if your OTP length is different. const otpRegex = /^\d{6}$/; // Adjust '6' if needed // Basic OTP format validation if (!otpRegex.test(otp)) { return res.status(400).json({ message: 'Invalid OTP format. Expecting 6 digits.' }); // Adjust message if length changes } console.log(`Verifying OTP for session: ${session_uuid}`); try { const response = await client.verify.session.validate( session_uuid, // The session_uuid received from the create step { otp: otp } // The OTP entered by the user // See Plivo Verify docs: https://www.plivo.com/docs/verify/api/session#validate-a-session ); console.log(""Plivo Validate Session Response:"", response); // *** Validation Check *** // Checking the response message string is functional but might be brittle // if Plivo changes the exact wording. // Ideally, check Plivo's documentation for a more robust success indicator, // such as a specific status code within the success response body or a boolean flag, // if available. Using the message is a fallback. if (response && response.message === ""Session validated successfully."") { res.status(200).json({ verified: true, message: response.message }); } else { // Handle cases where validation fails (e.g., wrong OTP, expired session) // Plivo might return a specific message or status code here console.warn(""OTP Validation Failed:"", response); res.status(400).json({ verified: false, message: response.message || 'Invalid or expired OTP.' }); } } catch (error) { console.error('Plivo API Error validating session:', error); let errorMessage = 'Failed to verify OTP. Please try again later.'; let statusCode = 500; // Plivo might throw specific errors for invalid session UUID, expired session, etc. // Check error structure and provide more specific feedback if possible. // Example: A 404 might indicate an invalid or expired session UUID. if (error.statusCode === 404) { statusCode = 404; errorMessage = 'Invalid or expired session. Please request a new OTP.'; } else if (error.statusCode === 400) { // This might occur for incorrect OTP format even if basic check passed, or other issues statusCode = 400; errorMessage = 'Invalid OTP provided.'; // Check error.message for more details if needed console.error(""Detailed Plivo 400 Error:"", error.message); } else { console.error(""Detailed Plivo Error:"", error.message || error); } res.status(statusCode).json({ verified: false, message: errorMessage }); } }
Explanation:
- Similar setup for Plivo client initialization and
POST
method check. - Extracts
session_uuid
andotp
from the request body. Includes validation for presence and basic OTP format. - OTP Length: A comment highlights the 6-digit assumption and advises checking Plivo settings, adjusting the regex (
/^\d{6}$/
) and frontend accordingly. client.verify.session.validate()
is called with thesession_uuid
and theotp
.- Validation Logic: The code checks the success message string, but a comment clarifies this might be brittle and recommends checking Plivo documentation for potentially more robust validation methods (status codes, boolean flags).
- If validation fails (wrong OTP, expired session), Plivo might return a non-error response with a failure message, or throw a specific error (like 404 for invalid session). The code handles both possibilities, returning
{ verified: false }
. - The
catch
block handles API communication errors or unexpected Plivo errors during validation, logging details server-side and returning appropriate client-side messages (e.g., specific messages for 404 or 400 errors).
- Similar setup for Plivo client initialization and
3. Building the Frontend UI
Now let's create the React component for the user interface on the main page.
-
Edit the main page:
pages/index.js
-
Replace its content with the following:
// pages/index.js import { useState } from 'react'; import Head from 'next/head'; // Define OTP length constant (adjust if needed based on Plivo config) const OTP_LENGTH = 6; export default function Home() { const [phoneNumber, setPhoneNumber] = useState(''); const [otp, setOtp] = useState(''); const [sessionUuid, setSessionUuid] = useState(''); const [message, setMessage] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isOtpSent, setIsOtpSent] = useState(false); const [isVerified, setIsVerified] = useState(false); // Handler to request OTP const handleSendOtp = async (e) => { e.preventDefault(); setIsLoading(true); setMessage(''); setError(''); setOtp(''); // Clear previous OTP input if any setIsVerified(false); // Reset verification status // Basic client-side validation (improve with libraries like react-phone-number-input if needed) if (!phoneNumber || !/^\+?[1-9]\d{1,14}$/.test(phoneNumber)) { setError('Please enter a valid phone number in E.164 format (e.g., +14155552671).'); setIsLoading(false); return; } try { const res = await fetch('/api/otp/send', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ recipient: phoneNumber }), }); const data = await res.json(); if (!res.ok) { // Use error message from API response if available throw new Error(data.message || `Error: ${res.status}`); } setSessionUuid(data.session_uuid); setMessage('OTP sent successfully! Please check your phone.'); setIsOtpSent(true); // Show OTP input field } catch (err) { console.error(""Send OTP Error:"", err); setError(err.message || 'Failed to send OTP. Please try again.'); setIsOtpSent(false); // Hide OTP input on error } finally { setIsLoading(false); } }; // Handler to verify OTP const handleVerifyOtp = async (e) => { e.preventDefault(); setIsLoading(true); setMessage(''); setError(''); setIsVerified(false); // Reset verification status // Use the OTP_LENGTH constant for validation if (!otp || !new RegExp(`^\\d{${OTP_LENGTH}}$`).test(otp)) { setError(`Please enter the ${OTP_LENGTH}-digit OTP.`); setIsLoading(false); return; } try { const res = await fetch('/api/otp/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ session_uuid: sessionUuid, otp: otp }), }); const data = await res.json(); if (!res.ok || !data.verified) { // Use error message from API response if available throw new Error(data.message || `Error: ${res.status}`); } setMessage('Phone number verified successfully!'); setIsVerified(true); // Indicate success // In a real app: redirect, update user profile, grant access, etc. // setSessionUuid(''); // Optional: Clear session UUID after successful verification // setIsOtpSent(false); // Optional: Hide OTP form after success } catch (err) { console.error(""Verify OTP Error:"", err); setError(err.message || 'Failed to verify OTP. Please check the code or request a new one.'); setIsVerified(false); } finally { setIsLoading(false); } }; return ( <div style={styles.container}> <Head> <title>Plivo OTP Verification</title> <meta name=""description"" content=""Next.js Plivo OTP Verification Example"" /> <link rel=""icon"" href=""/favicon.ico"" /> </Head> <main style={styles.main}> <h1 style={styles.title}>Phone Verification</h1> {!isVerified ? ( <> {/* Phone Number Input Form */} <form onSubmit={handleSendOtp} style={styles.form}> <label htmlFor=""phone"" style={styles.label}>Enter Phone Number (E.164 format):</label> <input type=""tel"" id=""phone"" value={phoneNumber} onChange={(e) => setPhoneNumber(e.target.value)} placeholder=""+14155552671"" required disabled={isLoading || isOtpSent} // Disable while loading or after OTP sent initially style={styles.input} /> <button type=""submit"" disabled={isLoading || !phoneNumber} style={styles.button}> {isLoading && !isOtpSent ? 'Sending...' : 'Send OTP'} </button> </form> {/* OTP Input Form - Shown only after OTP is requested */} {isOtpSent && ( <form onSubmit={handleVerifyOtp} style={styles.form}> <label htmlFor=""otp"" style={styles.label}>Enter OTP:</label> <input type=""text"" // Use ""text"" and pattern for better mobile input handling inputMode=""numeric"" // Hint for numeric keyboard pattern={`\\d{${OTP_LENGTH}}`} // Enforce OTP_LENGTH digits maxLength={OTP_LENGTH} // Use constant for max length id=""otp"" value={otp} onChange={(e) => setOtp(e.target.value)} required disabled={isLoading} style={styles.input} /> <button type=""submit"" disabled={isLoading || !otp || otp.length < OTP_LENGTH} style={styles.button}> {isLoading ? 'Verifying...' : 'Verify OTP'} </button> {/* Optional: Add a ""Resend OTP"" button here */} </form> )} </> ) : ( <div style={styles.successMessage}> Phone Number Verified Successfully! </div> )} {/* Display Messages and Errors */} {message && !error && <p style={styles.message}>{message}</p>} {error && <p style={styles.error}>{error}</p>} </main> </div> ); } // Basic inline styles for demonstration const styles = { container: { minHeight: '100vh', padding: '0 0.5rem', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', fontFamily: 'Arial, sans-serif', }, main: { padding: '2rem', border: '1px solid #eaeaea', borderRadius: '10px', maxWidth: '400px', width: '100%', textAlign: 'center', }, title: { margin: '0 0 1.5rem 0', lineHeight: '1.15', fontSize: '2rem', }, form: { display: 'flex', flexDirection: 'column', gap: '1rem', marginBottom: '1rem', }, label: { fontWeight: 'bold', textAlign: 'left', fontSize: '0.9rem', }, input: { padding: '0.8rem', border: '1px solid #ccc', borderRadius: '4px', fontSize: '1rem', }, button: { padding: '0.8rem 1.5rem', backgroundColor: '#0070f3', color: 'white', border: 'none', borderRadius: '4px', fontSize: '1rem', cursor: 'pointer', transition: 'background-color 0.2s ease', // Use conditional styling or CSS classes for disabled state }, // Example conditional style for disabled button (apply via style prop) // buttonDisabled: { // backgroundColor: '#ccc', // cursor: 'not-allowed', // }, message: { color: 'green', marginTop: '1rem', }, error: { color: 'red', marginTop: '1rem', }, successMessage: { color: 'green', marginTop: '1rem', fontSize: '1.2rem', fontWeight: 'bold', } };
Explanation:
- We use
useState
hooks to manage the phone number input, OTP input, session UUID, loading state, UI messages, errors, and whether the OTP form should be visible (isOtpSent
), and verification status (isVerified
). - An
OTP_LENGTH
constant is defined (defaulting to 6) for easier adjustment and used in validation and input attributes. handleSendOtp
:- Performs basic client-side validation on the phone number (E.164 format check). Note: Use a dedicated library like
react-phone-number-input
for robust international phone number validation in production. - Sends a
POST
request to/api/otp/send
with the phone number. - Handles the response: stores the
session_uuid
, shows a success message, and setsisOtpSent
to true to display the OTP input form. - Handles errors: displays an error message from the API or a generic one.
- Performs basic client-side validation on the phone number (E.164 format check). Note: Use a dedicated library like
handleVerifyOtp
:- Performs client-side validation on the OTP using the
OTP_LENGTH
constant. - Sends a
POST
request to/api/otp/verify
with thesession_uuid
andotp
. - Handles the response: If
data.verified
is true, shows a success message and setsisVerified
. Otherwise, throws an error. - Handles errors: displays an error message.
- Performs client-side validation on the OTP using the
- The JSX renders the forms conditionally based on
isOtpSent
andisVerified
. InputmaxLength
,pattern
, and validation logic now use theOTP_LENGTH
constant. - Basic inline styles are provided. Use CSS Modules, Tailwind CSS, or conditional styling for disabled states in real applications.
- We use
4. Integrating with Plivo (Credentials)
You've already set up the environment variables, but let's ensure you know where to find the Plivo credentials.
-
Log in to your Plivo Console: Go to console.plivo.com.
-
Navigate to API Keys: On the main dashboard or via the navigation menu, find the section related to ""API"" or ""Account"" and look for ""Keys & Credentials"" or similar.
-
Copy Auth ID and Auth Token: You will see your
AUTH ID
andAUTH TOKEN
. Copy these values. -
Update
.env.local
: Paste the copied values into your.env.local
file:PLIVO_AUTH_ID=YOUR_ACTUAL_AUTH_ID_FROM_PLIVO PLIVO_AUTH_TOKEN=YOUR_ACTUAL_AUTH_TOKEN_FROM_PLIVO
CRITICAL: You must replace the placeholder text above with your actual credentials obtained from the Plivo console for the application to function.
-
Restart your Next.js server: Environment variables in
.env.local
are only loaded at build time or server start. Stop your development server (Ctrl+C) and restart it:npm run dev # or yarn dev
Secure Handling:
- Never commit
.env.local
or your actual credentials to version control (Git). - Use your hosting provider's mechanism for setting environment variables in staging and production environments (e.g., Vercel Environment Variables, Netlify Build environment variables).
5. Error Handling, Logging, and Retries
We've added basic error handling, but let's refine it.
- Consistent Error Strategy: Both API routes and the frontend catch errors. API routes log detailed errors server-side and return user-friendly messages. The frontend displays these messages.
- Logging:
- We are using
console.log
andconsole.error
in the API routes. For production, use a structured logging library likepino
orwinston
. This enables better log parsing, filtering, and integration with log management services (e.g., Datadog, Logtail). - Log crucial events: OTP request initiation, Plivo API success/failure responses, validation attempts, and final verification status.
- We are using
- Retry Mechanisms:
- Client-Side (User Initiated): The current setup allows the user to manually retry sending the OTP or verifying it if an error occurs. Consider adding an explicit ""Resend OTP"" button. This button should likely call the
/api/otp/send
endpoint again. Implement a cooldown period (e.g., disable the button for 30-60 seconds after a request) to prevent abuse. - Server-Side (API Call Retries): For transient network errors when calling the Plivo API, you could implement automatic retries with exponential backoff within the API route (using libraries like
async-retry
). However, for OTP, be cautious: retrying acreate
call might result in multiple OTPs being sent (and billed). Retrying avalidate
call is generally safer if the initial attempt failed due to a network issue. Start simple; add server-side retries only if transient Plivo API call failures become a noticeable problem.
- Client-Side (User Initiated): The current setup allows the user to manually retry sending the OTP or verifying it if an error occurs. Consider adding an explicit ""Resend OTP"" button. This button should likely call the
Testing Error Scenarios:
- Enter an incorrectly formatted phone number.
- Enter a valid number but an incorrect OTP.
- Wait for the OTP session to expire (Plivo sessions typically last a few minutes) and then try to verify.
- Temporarily put incorrect Plivo credentials in
.env.local
to simulate auth errors. - Simulate network errors (e.g., using browser developer tools network throttling) when the frontend calls the API routes.
6. Database Schema and Data Layer (Considerations)
This specific guide focuses solely on OTP verification and doesn't implement a full user database. However, in a production application, you would integrate this flow with your user management system:
- User Schema: Your user table/document would likely have fields like:
id
email
/username
password_hash
phone_number
(string, store in E.164 format)is_phone_verified
(boolean, defaults tofalse
)two_factor_enabled
(boolean, defaults tofalse
)created_at
,updated_at
- Workflow Integration:
- Sign-up: User provides phone number -> Send OTP -> Verify OTP -> If successful, set
is_phone_verified = true
for the newly created user record. - Login (2FA): User logs in with password -> If
two_factor_enabled = true
, prompt for OTP -> Send OTP (using the user's verifiedphone_number
) -> Verify OTP -> If successful, grant access (create session/JWT).
- Sign-up: User provides phone number -> Send OTP -> Verify OTP -> If successful, set
- Data Layer: Use an ORM (like Prisma, TypeORM, Mongoose) or a query builder to interact with your database securely and efficiently.
For this example, the state (isVerified
) is temporary and managed in the React component. A real app needs persistence.
7. Adding Security Features
Security is paramount for authentication flows.
- Input Validation and Sanitization:
- Server-Side: The API routes have basic checks (
!recipient
,!session_uuid
,!otp
). Enhance phone number validation using a robust library on the server (e.g., Google'slibphonenumber
via a Node.js port) to ensure format correctness before calling Plivo. Validatesession_uuid
format (it's typically a UUID). Ensure OTP is numeric and matches expected length (as implemented in 2.2). - Client-Side: Basic checks help user experience but should always be duplicated server-side.
- Server-Side: The API routes have basic checks (
- Rate Limiting: This is critical to prevent abuse (attackers spamming phone numbers with OTPs, incurring costs) and brute-forcing OTPs.
- Apply rate limiting to both
/api/otp/send
and/api/otp/verify
endpoints. - Implement limits based on IP address, user ID (if logged in), or phone number.
- Examples:
- Limit
/api/otp/send
to X requests per phone number per hour. - Limit
/api/otp/verify
to Y attempts persession_uuid
. - Limit overall requests per IP address.
- Limit
- Use libraries like
rate-limiter-flexible
orexpress-rate-limit
(if using Express middleware with Next.js, or adapt the logic). Vercel and Netlify also offer built-in or add-on solutions for edge rate limiting. - Example using
rate-limiter-flexible
(conceptual):// pages/api/otp/send.js (or verify.js) - Simplified Example import { Client } from 'plivo'; import { RateLimiterMemory } from 'rate-limiter-flexible'; // Configure rate limiter (e.g., 5 requests per IP per minute) // Store this limiter instance appropriately (e.g., outside handler for reuse) const rateLimiter = new RateLimiterMemory({ points: 5, // Number of points duration: 60, // Per second(s) }); // ... (Plivo client setup) ... export default async function handler(req, res) { if (req.method !== 'POST') { /* ... */ } const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; try { await rateLimiter.consume(ip); // Consume 1 point per request for this IP } catch (rejRes) { console.warn(`Rate limit exceeded for IP: ${ip}`); return res.status(429).json({ message: 'Too Many Requests' }); } // --- Proceed with OTP logic if rate limit not exceeded --- const { recipient } = req.body; // ... validation ... try { // ... Plivo API call ... res.status(200).json({ /* ... */ }); } catch (error) { // ... error handling ... res.status(500).json({ message: 'Failed to process request' }); } }
- Apply rate limiting to both