Node.js Developer Guide

The Complete Guide to Sending OTPs in Node.js & Express

A production-ready, step-by-step technical guide to integrating the NepalOTP API into your Node.js backend. Learn how to handle requests, prevent abuse with Redis, and verify users securely.

Implementing phone verification from scratch is deceptively complex. It requires handling external API requests, generating cryptographically secure random codes, hashing them securely in your database, handling strict expirations, and managing retry attempts to prevent brute-force attacks.

By utilizing the NepalOTP REST API, all of this stateful logic is offloaded to our highly-available infrastructure. This guide will walk you through building a robust, production-grade Express.js controller to handle phone verification.


Prerequisites & Environment Setup

Before writing any code, ensure your environment is prepared for secure API communication.

  • Node.js 18+ (Native fetch is supported, but we will use axios for comprehensive error handling).
  • An active NepalOTP account. Generate a Sandbox API Key from your dashboard to test without spending credits.
  • Redis (Optional but recommended) for caching and rate-limiting.

Install the required dependencies via NPM or Yarn:

Terminal
npm install express axios dotenv joi

1. Validating and Requesting the OTP

The first step in any authentication flow is sanitizing user input. Never trust the phone number provided by the client. We will use Joi to ensure the phone number matches the strict Nepali E.164 format (e.g., +97798XXXXXXXX).

Once validated, we make a POST request to the NepalOTP /v1/otp/send endpoint. The API will respond with an id which we must return to the client.

controllers/auth.controller.js
const axios = require('axios');
const Joi = require('joi');

// Define strict validation for Nepali phone numbers
const phoneSchema = Joi.object({
    phone: Joi.string().pattern(new RegExp('^\\+977\\d{10}$')).required()
});

exports.requestOtp = async (req, res) => {
    try {
        // 1. Validate Input
        const { error, value } = phoneSchema.validate(req.body);
        if (error) return res.status(400).json({ error: 'Invalid phone number format. Use +977...' });

        // 2. Send Request to NepalOTP
        const response = await axios.post('https://api.nepalotp.com/v1/otp/send', {
            phone: value.phone
        }, {
            headers: {
                'Authorization': `Bearer ${process.env.NEPALOTP_API_KEY}`,
                'Content-Type': 'application/json'
            },
            // Always set timeouts for external API calls
            timeout: 5000 
        });

        // 3. Return the OTP ID to the client frontend
        return res.json({ 
            success: true, 
            otp_id: response.data.id,
            expires_at: response.data.expires_at,
            message: 'Verification code dispatched.'
        });
        
    } catch (err) {
        // 4. Graceful Error Handling
        if (err.response) {
            // The API responded with a 4xx/5xx code (e.g., Rate Limited, Insufficient Balance)
            return res.status(err.response.status).json({ 
                error: err.response.data.message || 'Failed to dispatch OTP' 
            });
        }
        // Network error or timeout
        return res.status(503).json({ error: 'Service temporarily unavailable' });
    }
};

2. Verifying the Code

When the user receives the SMS, they will input the 6-digit code into your frontend application. The frontend must send this code, along with the otp_id from the previous step, back to your Express server.

Your server forwards these details to the /v1/otp/verify endpoint. NepalOTP automatically checks if the code matches, if it has expired, or if the user has exhausted their maximum retry attempts.

controllers/auth.controller.js
exports.verifyOtp = async (req, res) => {
    try {
        const { otp_id, code } = req.body;
        
        if (!otp_id || !code) {
            return res.status(400).json({ error: 'Missing required fields.' });
        }

        // Check code against NepalOTP
        const response = await axios.post('https://api.nepalotp.com/v1/otp/verify', {
            id: otp_id,
            code: code
        }, {
            headers: {
                'Authorization': `Bearer ${process.env.NEPALOTP_API_KEY}`
            }
        });

        if (response.data.success) {
            // Authentication successful! The phone number is verified.
            // Business Logic: Find or create the user in your database
            const phone = response.data.data.phone;
            
            // Example: const user = await User.findOrCreate({ phone });
            // Example: const token = generateJwt(user);

            return res.json({ 
                success: true, 
                message: 'Phone number verified successfully',
                // token: token
            });
        }
        
    } catch (err) {
        // Handles expired codes, invalid codes, or max attempts reached automatically
        const errorMessage = err.response?.data?.message || 'Verification failed';
        const attemptsRemaining = err.response?.data?.data?.attempts_remaining;
        
        return res.status(400).json({ 
            error: errorMessage,
            attempts_left: attemptsRemaining 
        });
    }
};

3. Advanced: Receiving Webhooks

For enterprise applications, you should not assume an SMS was delivered just because the API responded with a 200 OK. Telecom networks can fail downstream. NepalOTP provides asynchronous webhooks to inform your server of the exact delivery status (e.g., delivered, failed, rejected).

Here is how you can set up a secure Express route to receive these webhooks, verifying the cryptographic signature to ensure the payload is genuinely from NepalOTP.

routes/webhooks.js
const crypto = require('crypto');
const express = require('express');
const router = express.Router();

// You MUST use express.raw to access the raw payload string for signature verification
router.post('/nepalotp', express.raw({type: 'application/json'}), (req, res) => {
    const signature = req.headers['x-npot-signature'];
    const timestamp = req.headers['x-npot-timestamp'];
    const webhookSecret = process.env.NEPALOTP_WEBHOOK_SECRET;

    // 1. Prevent Replay Attacks (Check if timestamp is within 5 minutes)
    const now = Math.floor(Date.now() / 1000);
    if (now - parseInt(timestamp) > 300) {
        return res.status(400).send('Webhook expired');
    }

    // 2. Verify Cryptographic Signature
    const payloadString = timestamp + '.' + req.body.toString();
    const expectedSignature = crypto
        .createHmac('sha256', webhookSecret)
        .update(payloadString)
        .digest('hex');

    const header = `t=${timestamp},v1=${expectedSignature}`;

    if (header !== signature) {
        return res.status(401).send('Invalid signature');
    }

    // 3. Process the Webhook payload securely
    const eventData = JSON.parse(req.body.toString());
    
    if (eventData.event === 'sms.delivered') {
        console.log(`SMS ${eventData.data.sms_id} was successfully delivered.`);
        // Update your database record here
    }

    return res.status(200).send('OK');
});

Best Practices & FAQs

How do I prevent OTP spam or SMS pumping?

SMS pumping is a type of toll fraud where bots aggressively request OTPs to generate revenue for shady international carriers. To protect your application and API balance:

  • Implement IP-based rate limiting on your /request-otp route using libraries like express-rate-limit.
  • Enforce a strict cooldown period (e.g., 60 seconds) before a user can request a "Resend". You can track this state in Redis or on the frontend.
  • Utilize NepalOTP's built-in global rate limits which automatically block abusive numbers at the network level.

Can I test this locally without spending credits?

Yes. Log into your dashboard and generate a Sandbox API Key. Replace your live key with this sandbox key in your .env. The API will perform all validations, enforce rate limits, and generate mock OTP codes, but it will not dispatch a physical SMS to the carrier, costing you zero credits.

Ship authentication faster.

Get your sandbox API keys instantly and integrate our endpoints into your Express.js application in minutes.

Create Developer Account