code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

How to Send SMS with Plivo Node.js & Express: Complete Tutorial (2025)

A step-by-step guide to building a Node.js Express application for sending SMS messages using the Plivo API, covering setup, implementation, security, and testing.

Send SMS Messages with Node.js, Express, and Plivo

Learn how to send SMS messages programmatically using Node.js, Express.js, and the Plivo messaging API. This comprehensive tutorial walks you through building a production-ready SMS application from scratch, covering authentication, error handling, security, deployment, and 10DLC compliance for US messaging.

By the end of this guide, you'll have a fully functional REST API endpoint that can send text messages to any phone number worldwide using Plivo's cloud communications platform. Perfect for implementing two-factor authentication (2FA), transactional notifications, appointment reminders, or marketing campaigns.

Framework Compatibility Note (2025): This guide supports both Express 4.x and Express 5.x. Express 5.1.0 became the default on npm as of March 31, 2025, and requires Node.js 18 or higher. Express 5 includes breaking changes: req.param() is removed (use req.params, req.body, or req.query explicitly), and promise rejections automatically forward to error-handling middleware. If using Express 5, review the Express 5 migration guide.

What You'll Build: SMS API Project Overview

Goal: Create a secure and robust Node.js Express API endpoint (POST /send-sms) that accepts a recipient phone number and message body, then uses the Plivo API to send the SMS.

Problem Solved: Provides a backend service to programmatically send SMS messages, abstracting direct Plivo API interaction behind a simple web service.

Real-World Use Cases:

  • Two-factor authentication (2FA) and one-time passwords (OTP)
  • Order confirmations and shipping notifications for e-commerce
  • Appointment reminders for healthcare and service businesses
  • Emergency alerts and critical system notifications
  • Marketing campaigns and promotional messages

Technologies:

TechnologyPurposeVersion Notes (2025)
Node.jsJavaScript runtime for server-side applications. Asynchronous, event-driven architecture suits I/O operations like API calls.v22 (LTS, active until Oct 2027) or v20 (LTS, maintenance until Apr 2026)
Express.jsMinimal web application framework for routes and HTTP request handling.v5.1.0 is default (as of Mar 31, 2025); v4.x in maintenance mode
PlivoCloud communications platform providing SMS and Voice APIs.Current API version
Plivo Node.js SDKSimplifies interaction with the Plivo REST API.Latest: ^4.x
dotenvLoads environment variables from .env file into process.env.^16.0.0 or later

System Architecture:

text
+-------------+       +-----------------------+       +-----------------+       +-----------------+
|             | HTTP  |                       | Plivo |                 | SMS   |                 |
| User/Client | ----> | Node.js/Express App   | ----> |   Plivo API     | ----> | Recipient Phone |
| (e.g. curl) | POST  | (API Endpoint /send-sms)| SDK   | (Sends Message) |       |                 |
+-------------+       +-----------------------+       +-----------------+       +-----------------+
                        | Loads Credentials
                        | from .env
                        v
                    +---------+
                    |  .env   |
                    | (Secrets)|
                    +---------+

Prerequisites:

  • Node.js and npm (or yarn): Download from nodejs.org. Recommended: Node.js v22 (LTS) or v20 (LTS) for production use in 2025.
  • Plivo Account: Sign up at plivo.com.
  • Plivo Auth ID and Auth Token: Available on your Plivo account dashboard.
  • Plivo Phone Number or Registered Sender ID:
    • US/Canada: Purchase an SMS-enabled Plivo phone number. For US: As of June 1, 2023, register your traffic for 10DLC (10-Digit Long Code) compliance to avoid carrier surcharges. Register your brand and campaign via Plivo Console → Messaging → 10DLC. Unregistered traffic incurs significant surcharges.
    • Other Countries: Use a registered Alphanumeric Sender ID (check Plivo's country-specific guidelines and register via Plivo support) or a purchased Plivo number.
  • Basic JavaScript and REST API knowledge.
  • (Optional) curl or Postman: For testing the API endpoint.

10DLC Registration Process (US Only):

  1. Register your brand (business entity) in Plivo Console → Messaging → 10DLC
  2. Create a campaign describing your use case (transactional, marketing, etc.)
  3. Wait for approval (typically 1–3 business days)
  4. Associate your Plivo phone number with the approved campaign
  5. Begin sending compliant traffic

Common 10DLC Issues:

  • High carrier surcharges despite registration: Verify campaign is approved and number is properly associated.
  • Low throughput limits: Consider using a Toll-Free number or Short Code for higher volume.

Trial Account Limitations: Plivo trial accounts can typically only send messages to phone numbers verified within your Plivo console (Sandbox Numbers).

SMS Pricing (US, 2025): Plivo charges $0.0055 per outbound SMS segment via long code, plus carrier surcharges ranging from $0.0030 (AT&T, T-Mobile) to $0.0050 (US Cellular, other carriers) per SMS for 10DLC-registered traffic. Check Plivo's SMS pricing page for current rates and international pricing.

Cost Calculation Examples:

  • Single SMS (160 characters or less): $0.0055 + $0.0030 = $0.0085 per message (AT&T/T-Mobile)
  • Multi-segment SMS (320 characters): 2 × $0.0085 = $0.017 per message
  • 1,000 messages/day: ~$8.50/day or $255/month (single segment, AT&T/T-Mobile)
  • Unicode/emoji messages (70 characters): Higher segment count increases costs proportionally

1. Setting up Your Node.js Express Project

Initialize your Node.js project and install the necessary dependencies.

  1. Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.

    bash
    mkdir plivo-node-sms-guide
    cd plivo-node-sms-guide
  2. Initialize Node.js Project: Create a package.json file to manage project dependencies and scripts.

    bash
    npm init -y

    (The -y flag accepts default settings)

  3. Install Dependencies: Install Express for the web server, the Plivo SDK to interact with their API, and dotenv to manage environment variables.

    bash
    npm install express@^5.1.0 plivo@^4.67.1 dotenv@^16.4.5

    Dependency Versions (2025):

    • express@^5.1.0: Latest stable Express 5.x (use ^4.21.2 for Express 4.x)
    • plivo@^4.67.1: Current Plivo SDK
    • dotenv@^16.4.5: Environment variable management
  4. Create Project Structure: Set up a basic structure for clarity.

    bash
    mkdir routes # To hold your API route definitions
    touch server.js .env .gitignore
    • server.js: The main entry point for your application.
    • routes/: Directory to organize route handlers.
    • .env: File to store sensitive credentials (API keys, etc.). Never commit this file to version control.
    • .gitignore: Specifies intentionally untracked files that Git should ignore.
  5. Configure .gitignore: Add node_modules and .env to prevent committing them. Open .gitignore and add:

    text
    # .gitignore
    
    # Dependencies
    node_modules
    
    # Environment variables
    .env
    
    # Logs
    logs
    *.log
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
  6. Set up Basic Express Server (server.js): Open server.js and add the initial Express setup.

    javascript
    // server.js
    require('dotenv').config(); // Load environment variables from .env file
    const express = require('express');
    
    const app = express();
    const PORT = process.env.PORT || 3000; // Use port from env var or default to 3000
    
    // Middleware to parse JSON request bodies
    app.use(express.json());
    // Middleware to parse URL-encoded request bodies
    app.use(express.urlencoded({ extended: true }));
    
    // Basic root route (optional)
    app.get('/', (req, res) => {
      res.send('Plivo SMS Sender API is running!');
    });
    
    // Placeholder for SMS routes (we'll add this soon)
    // const smsRoutes = require('./routes/sms');
    // app.use('/api', smsRoutes);
    
    // Start the server
    app.listen(PORT, () => {
      console.log(`Server is running on port ${PORT}`);
    });
    • require('dotenv').config();: Loads variables from .env. Call this early.
    • express.json() and express.urlencoded(): Middleware needed to parse incoming request bodies.
    • process.env.PORT || 3000: Allows configuring the port via environment variables, essential for deployment platforms.

2. How to Send SMS with Plivo API: Implementing Core Functionality

Create the logic to interact with the Plivo API using their Node.js SDK.

  1. Create SMS Route File: Inside the routes directory, create a file named sms.js.

    bash
    touch routes/sms.js
  2. Implement the Sending Logic (routes/sms.js): This file defines the router and the handler for your /send-sms endpoint.

    javascript
    // routes/sms.js
    const express = require('express');
    const plivo = require('plivo');
    
    const router = express.Router();
    
    // Validate environment variables
    const AUTH_ID = process.env.PLIVO_AUTH_ID;
    const AUTH_TOKEN = process.env.PLIVO_AUTH_TOKEN;
    const SENDER_NUMBER = process.env.PLIVO_SENDER_NUMBER; // Your Plivo number or Sender ID
    
    if (!AUTH_ID || !AUTH_TOKEN || !SENDER_NUMBER) {
      console.error('Error: Plivo credentials or sender number not configured in .env file.');
      // In a real app, you might prevent the server from starting or handle this more gracefully.
      // For this guide, we'll log the error and let requests fail if credentials aren't set.
    }
    
    // Initialize Plivo client (only if credentials exist)
    let client;
    if (AUTH_ID && AUTH_TOKEN) {
        client = new plivo.Client(AUTH_ID, AUTH_TOKEN);
    }
    
    // POST /api/send-sms
    router.post('/send-sms', async (req, res) => {
      // Basic input validation
      const { to, text } = req.body;
    
      if (!to || !text) {
        return res.status(400).json({ success: false, error: 'Missing required fields: "to" and "text".' });
      }
    
      // Basic E.164 format check (simple version)
      // A more robust validation library (like libphonenumber-js) is recommended for production,
      // as it handles varying international lengths, country codes, and number types more accurately.
      if (!/^\+\d{1,15}$/.test(to)) {
          return res.status(400).json({
              success: false,
              error: 'Invalid "to" number format. Use E.164 format (e.g., +14155552671).'
          });
      }
    
      if (!client) {
          console.error('Plivo client not initialized due to missing credentials.');
          return res.status(500).json({ success: false, error: 'Server configuration error: Plivo client not available.' });
      }
    
      try {
        console.log(`Attempting to send SMS to ${to} from ${SENDER_NUMBER}`);
    
        const response = await client.messages.create({
          src: SENDER_NUMBER, // Sender ID or Plivo Number from .env
          dst: to,           // Destination number from request body
          text: text,        // Message text from request body
          // Optional parameters:
          // type: 'sms',    // Message type (sms or mms)
          // url: 'https://your-callback.com/dlr', // Delivery receipt callback URL
          // method: 'POST', // Callback method
          // log: true,      // Enable logging
        });
    
        console.log('Plivo API Response:', response);
    
        // Plivo API generally returns a 202 Accepted on success
        // The response contains message UUIDs
        res.status(202).json({
          success: true,
          message: 'SMS send request accepted by Plivo.',
          plivoResponse: response,
        });
    
      } catch (error) {
        console.error('Error sending SMS via Plivo:', error);
    
        // Provide a more specific error message if possible
        const errorMessage = error.message || 'Failed to send SMS.';
        const statusCode = error.statusCode || 500; // Use Plivo's status code if available
    
        res.status(statusCode).json({
          success: false,
          error: 'Plivo API Error',
          details: errorMessage,
          plivoError: error // Include the raw error for debugging if appropriate
        });
      }
    });
    
    module.exports = router; // Export the router
    • Import express and plivo.
    • Create an express.Router().
    • Load Plivo credentials and the sender number from process.env. Add checks to ensure they exist.
    • Initialize the Plivo client only if credentials are present.
    • The POST /send-sms handler uses async/await for cleaner asynchronous code.
    • Input Validation: Basic checks ensure to and text are provided and to roughly matches E.164 format. A note about using better libraries like libphonenumber-js is included.
    • Plivo Call: client.messages.create() sends the SMS. Key parameters: src (sender), dst (recipient), text (message content). Optional parameters include type, url (delivery receipt callback), method, and log.
    • Response Handling: On success (Plivo returns 202 Accepted), send a success JSON response. On error, catch it, log it, and send an appropriate error JSON response including details from the Plivo error object.
  3. Mount the Router in server.js: Uncomment and add the lines in server.js to use the router you just created.

    javascript
    // server.js
    require('dotenv').config();
    const express = require('express');
    const smsRoutes = require('./routes/sms'); // Import the router
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));
    
    app.get('/', (req, res) => {
      res.send('Plivo SMS Sender API is running!');
    });
    
    // Mount the SMS routes under the /api path
    app.use('/api', smsRoutes); // Use the imported router
    
    app.listen(PORT, () => {
      console.log(`Server is running on port ${PORT}`);
    });

    Now, requests to POST /api/send-sms will be handled by your routes/sms.js logic.

3. Building and Testing Your SMS API Endpoint

You've already implemented the core API endpoint (POST /api/send-sms) in the previous step. This section details its usage and provides testing examples.

API Endpoint Specification:

PropertyValue
MethodPOST
Path/api/send-sms
Content-Typeapplication/json
AuthenticationNone (assumes internal or infrastructure-protected)

Request Body Parameters:

ParameterTypeRequiredDescriptionExample
tostringYesRecipient's phone number in E.164 format+14155552671
textstringYesSMS message contentHello from Plivo!

Response Status Codes:

Status CodeMeaningWhen It Occurs
202 AcceptedSMS queued successfullyValid request, Plivo accepted the message
400 Bad RequestInvalid inputMissing fields, invalid phone format
500 Internal Server ErrorServer or Plivo API errorConfiguration issues, Plivo service errors

Example curl Request:

Replace placeholders with your actual server address, recipient number, and message.

bash
curl -X POST http://localhost:3000/api/send-sms \
-H "Content-Type: application/json" \
-d '{
      "to": "+14155552671",
      "text": "Hello from your Node.js Plivo App!"
    }'

Example Success Response (HTTP 202 Accepted):

json
{
  "success": true,
  "message": "SMS send request accepted by Plivo.",
  "plivoResponse": {
    "message": "message(s) queued",
    "messageUuid": [
      "1f7a77ca-f1b7-11ee-9d7c-0242ac110003"
    ],
    "apiId": "1f7a6efc-f1b7-11ee-9d7c-0242ac110003"
  }
}

Example Error Response (HTTP 400 Bad Request – Missing Field):

json
{
  "success": false,
  "error": "Missing required fields: \"to\" and \"text\"."
}

Example Error Response (HTTP 400 Bad Request – Invalid Number Format):

json
{
    "success": false,
    "error": "Invalid \"to\" number format. Use E.164 format (e.g., +14155552671)."
}

Example Error Response (HTTP 500 Internal Server Error – Plivo API Failure):

json
{
  "success": false,
  "error": "Plivo API Error",
  "details": "Resource not found",
  "plivoError": { /* Full Plivo error object */ }
}

4. Plivo Authentication: Configuring Your API Credentials

Proper configuration and secure handling of credentials are critical.

  1. Obtain Plivo Credentials and Sender Number:

    • Log in to your Plivo Console.
    • Auth ID & Auth Token: Your Auth ID and Auth Token appear on the main dashboard (landing page after login).
    • Sender Number/ID:
      • For US/Canada: Go to Phone Numbers → Buy Numbers. Search for and purchase an SMS-enabled number in the desired country/region. Note the full number in E.164 format (e.g., +12025551234).
      • For other regions: Check Plivo's documentation for sender ID requirements. You might use a purchased Plivo number or register an Alphanumeric Sender ID via Messaging → Sender IDs → Add Sender ID (often requires approval).
  2. Configure Environment Variables (.env file): Open the .env file in your project's root directory and add your credentials. Replace the placeholder values.

    dotenv
    # .env - Plivo Configuration
    # DO NOT COMMIT THIS FILE TO VERSION CONTROL
    
    # Plivo API Credentials (from Plivo Console Dashboard)
    PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID_HERE
    PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN_HERE
    
    # Plivo Sender Number or Registered Sender ID
    # For US/Canada, use the E.164 format number you purchased (e.g., +12025551234)
    # For other regions, use your registered Alphanumeric Sender ID or a Plivo number
    PLIVO_SENDER_NUMBER=YOUR_PLIVO_NUMBER_OR_SENDER_ID
    • PLIVO_AUTH_ID: Your unique authentication identifier.
    • PLIVO_AUTH_TOKEN: Your secret authentication token. Treat this like a password.
    • PLIVO_SENDER_NUMBER: The number or ID messages will appear to come from. Must be a valid Plivo number (for US/Canada) or a registered Sender ID where applicable.
  3. Load Environment Variables: The require('dotenv').config(); line at the top of server.js loads these variables into process.env, making them accessible throughout your application (as seen in routes/sms.js).

Security: The .env file keeps your sensitive credentials out of your source code. Ensure your .gitignore file includes .env to prevent accidentally committing it. In production environments, set these variables directly in the deployment platform's configuration settings, not via a .env file.

Platform-Specific Configuration:

  • AWS: Use AWS Systems Manager Parameter Store or Secrets Manager
  • Heroku: Configure via Heroku Config Vars (heroku config:set PLIVO_AUTH_ID=xxx)
  • Azure: Use Azure Key Vault or App Service Configuration
  • Docker: Pass environment variables via -e flag or docker-compose.yml

Fallback Mechanisms: For this simple guide, there's no fallback. In a production system requiring high availability, consider:

  • Implementing retries (see next section).
  • Having a secondary messaging provider configured and switching if Plivo experiences an outage (requires more complex logic).

5. Error Handling Best Practices for SMS Applications

Robust error handling and logging are crucial for debugging and reliability.

  1. Error Handling Strategy:

    • Validation Errors: Return HTTP 400 Bad Request for invalid client input (missing fields, bad number format). Provide clear JSON error messages.
    • Configuration Errors: If Plivo credentials aren't set, log an error on startup and return HTTP 500 if an attempt occurs to use the uninitialized client.
    • Plivo API Errors: Catch errors from the client.messages.create() call. Log the detailed error. Return an appropriate HTTP status code (often 500 Internal Server Error, or use error.statusCode if Plivo's SDK provides it) and a JSON error message including details from the Plivo error.
    • Unexpected Errors: Use a global error handler in Express or ensure all async routes have try...catch blocks to prevent unhandled promise rejections from crashing the server.
  2. Implement Global Error Handler: Add this middleware at the end of server.js, after all routes:

    javascript
    // Global error handler (add after all routes in server.js)
    app.use((err, req, res, next) => {
      console.error('Unhandled error:', err);
    
      res.status(err.statusCode || 500).json({
        success: false,
        error: err.message || 'Internal server error',
        ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
      });
    });
    
    // Handle 404 errors
    app.use((req, res) => {
      res.status(404).json({
        success: false,
        error: 'Endpoint not found'
      });
    });
  3. Logging:

    • Current: We use console.log for successful operations and Plivo responses, and console.error for errors. This is suitable for development.
    • Production: Use a dedicated logging library like Winston or Pino. These offer:
      • Different log levels (debug, info, warn, error).
      • Structured logging (JSON format is common for easier parsing).
      • Multiple transports (log to console, files, external services).

    Example Winston Setup:

    javascript
    // logger.js
    const winston = require('winston');
    
    const logger = winston.createLogger({
      level: process.env.LOG_LEVEL || 'info',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
      ),
      transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' })
      ]
    });
    
    if (process.env.NODE_ENV !== 'production') {
      logger.add(new winston.transports.Console({
        format: winston.format.simple()
      }));
    }
    
    module.exports = logger;

    Usage:

    javascript
    const logger = require('./logger');
    
    logger.info('Attempting to send SMS', { to, from: SENDER_NUMBER });
    logger.error('Plivo API error', { error: error.message, statusCode: error.statusCode });
  4. Retry Mechanisms:

    • Sending an SMS is often idempotent from Plivo's side (sending the same request usually doesn't result in duplicate messages if the first was accepted), but network issues could prevent the request from reaching Plivo.

    • For critical messages, implement a simple retry strategy with exponential backoff for network errors or specific transient Plivo errors (e.g., 5xx status codes). Libraries like async-retry can help.

    • Example (Conceptual):

      javascript
      // Conceptual retry logic using async-retry
      const retry = require('async-retry');
      // ... inside the route handler ...
      try {
          await retry(async bail => {
              try {
                  const response = await client.messages.create(/* ... */);
                  console.log('Plivo API Response:', response);
                  return response;
              } catch (error) {
                  // Don't retry for permanent errors
                  if (error.statusCode >= 400 && error.statusCode < 500) {
                      bail(new Error(`Non-retryable Plivo error: ${error.message}`));
                  }
                  // Retry for 5xx errors or network issues
                  console.warn(`Retrying Plivo request due to error: ${error.message}`);
                  throw error; // Trigger retry
              }
          }, {
              retries: 3,
              factor: 2,
              minTimeout: 1000,
          });
          res.status(202).json(/* ... */);
      } catch (error) {
          console.error('Error sending SMS via Plivo after retries:', error);
          res.status(error.statusCode || 500).json(/* ... */);
      }
    • Caution: Don't retry indefinitely or for errors that won't resolve (like invalid credentials or invalid recipient numbers).

Common Plivo Error Codes:

Error CodeDescriptionRecommended Action
400Bad Request – invalid parametersFix request format, don't retry
401Unauthorized – invalid credentialsCheck Auth ID/Token, don't retry
404Resource not foundVerify phone number/sender ID, don't retry
429Too many requests – rate limit exceededImplement backoff, retry after delay
500/502/503Server errorsRetry with exponential backoff
  1. Testing Error Scenarios:
    • Send requests missing to or text.
    • Send requests with invalid to number formats.
    • Temporarily modify .env with incorrect PLIVO_AUTH_ID or PLIVO_AUTH_TOKEN to test authentication failures.
    • Temporarily modify PLIVO_SENDER_NUMBER to an invalid one.
    • (If possible) Test with a recipient number known to cause issues (e.g., landline, blocked number).
    • Simulate network issues if testing infrastructure allows.

6. Database Schema and Data Layer

For this specific guide (only sending an SMS via an API call), a database is not strictly required.

However, in a real-world application, add a database (like PostgreSQL, MySQL, MongoDB) to:

  • Log Sent Messages: Store details like messageUuid, recipient, sender, timestamp, status (initially "queued", potentially updated via webhooks), and message content. This aids auditing and tracking.
  • Manage Users/Contacts: Store recipient information if sending to registered users.
  • Queue Messages: Store messages to be sent asynchronously by a background worker, especially for bulk sending or to decouple the API response from the actual sending process.

Example Database Schema (PostgreSQL):

sql
CREATE TABLE messages (
  id SERIAL PRIMARY KEY,
  plivo_message_uuid VARCHAR(255) UNIQUE NOT NULL,
  recipient_number VARCHAR(20) NOT NULL,
  sender_id VARCHAR(20) NOT NULL,
  message_text TEXT NOT NULL,
  status VARCHAR(50) DEFAULT 'queued',
  error_message TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  delivered_at TIMESTAMP
);

CREATE INDEX idx_messages_status ON messages(status);
CREATE INDEX idx_messages_recipient ON messages(recipient_number);
CREATE INDEX idx_messages_created_at ON messages(created_at DESC);

Field Descriptions:

FieldTypePurpose
idSERIALPrimary key
plivo_message_uuidVARCHAR(255)Unique identifier from Plivo
recipient_numberVARCHAR(20)E.164 formatted phone number
sender_idVARCHAR(20)Sender number or alphanumeric ID
message_textTEXTMessage content
statusVARCHAR(50)Message status (queued, sent, delivered, failed)
error_messageTEXTError details if message failed
created_atTIMESTAMPWhen message was created
updated_atTIMESTAMPLast status update
delivered_atTIMESTAMPWhen message was delivered

(Full implementation details omitted as they are beyond the scope of this basic SMS sending guide).

7. Security Best Practices for SMS APIs

Enhance the security of your API endpoint.

  1. Input Validation and Sanitization:

    • Validation: We already implemented basic validation for to and text. For production, use a robust validation library like joi or express-validator.

    Example with Joi:

    javascript
    const Joi = require('joi');
    
    const smsSchema = Joi.object({
      to: Joi.string()
        .pattern(/^\+\d{1,15}$/)
        .required()
        .messages({
          'string.pattern.base': 'Phone number must be in E.164 format (e.g., +14155552671)',
          'any.required': 'Recipient phone number is required'
        }),
      text: Joi.string()
        .min(1)
        .max(1600)
        .required()
        .messages({
          'string.max': 'Message text must not exceed 1600 characters',
          'any.required': 'Message text is required'
        })
    });
    
    // In route handler:
    const { error, value } = smsSchema.validate(req.body);
    if (error) {
      return res.status(400).json({
        success: false,
        error: error.details[0].message
      });
    }
  2. Common Vulnerabilities & Protections:

    • Authentication/Authorization: This guide omits direct API authentication, assuming protection via infrastructure. For many real-world uses, especially if the API isn't purely internal, implement authentication. Simple methods include checking for a secret API key in headers (X-API-Key) or using more robust methods like JWT.
    • Rate Limiting: Protect against brute-force attacks or abuse where users flood the API with requests.
    • Security Headers: Use headers like X-Content-Type-Options, Referrer-Policy, Strict-Transport-Security, X-Frame-Options to mitigate common web vulnerabilities.
  3. Implement Rate Limiting: Use a library like express-rate-limit.

    • Install: npm install express-rate-limit

    • Apply in server.js:

      javascript
      // server.js
      require('dotenv').config();
      const express = require('express');
      const rateLimit = require('express-rate-limit'); // Import
      const smsRoutes = require('./routes/sms');
      
      const app = express();
      const PORT = process.env.PORT || 3000;
      
      // Apply rate limiting to API routes
      const apiLimiter = rateLimit({
        windowMs: 15 * 60 * 1000, // 15 minutes
        max: 100, // Limit each IP to 100 requests per windowMs
        message: 'Too many 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
      });
      
      app.use('/api', apiLimiter); // Apply the limiter to /api routes
      
      app.use(express.json());
      app.use(express.urlencoded({ extended: true }));
      
      // Basic root route
      app.get('/', (req, res) => {
        res.send('Plivo SMS Sender API is running!');
      });
      
      app.use('/api', smsRoutes);
      
      app.listen(PORT, () => {
        console.log(`Server is running on port ${PORT}`);
      });

      Adjust windowMs and max according to your expected usage and security requirements.

  4. Implement Basic Security Headers: Use the helmet middleware.

    • Install: npm install helmet

    • Apply in server.js:

      javascript
      // server.js
      require('dotenv').config();
      const express = require('express');
      const rateLimit = require('express-rate-limit');
      const helmet = require('helmet'); // Import
      const smsRoutes = require('./routes/sms');
      
      const app = express();
      const PORT = process.env.PORT || 3000;
      
      app.use(helmet()); // Use Helmet for basic security headers
      
      const apiLimiter = rateLimit({
        windowMs: 15 * 60 * 1000, // 15 minutes
        max: 100, // Limit each IP to 100 requests per windowMs
        message: 'Too many 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
      });
      app.use('/api', apiLimiter);
      
      app.use(express.json());
      app.use(express.urlencoded({ extended: true }));
      
      // Basic root route
      app.get('/', (req, res) => {
        res.send('Plivo SMS Sender API is running!');
      });
      
      app.use('/api', smsRoutes);
      
      app.listen(PORT, () => {
        console.log(`Server is running on port ${PORT}`);
      });

      helmet() applies several default security-related HTTP headers.

  5. Protecting API Keys: Already covered by using .env and .gitignore. Ensure production environments use secure configuration management.

  6. Testing for Vulnerabilities:

    • Use security scanning tools (like OWASP ZAP, Burp Suite) against your deployed application (in a test environment).
    • Perform manual penetration testing focusing on input validation, authentication/authorization (if added), and potential injection points.
    • Check dependency vulnerabilities regularly (npm audit).

8. Understanding SMS Message Format and Compliance

E.164 Format: Strictly enforce the + prefix and country code format for to numbers. Plivo requires this. Consider libphonenumber-js for robust parsing and validation, as it understands nuances like variable number lengths per country.

Character Encoding (GSM vs. Unicode):

EncodingCharacters per SegmentTriggers
GSM 03.38160Standard Latin characters, basic punctuation
Unicode (UCS-2)70Emojis, non-Latin characters (Arabic, Chinese, etc.)

Plivo handles encoding automatically based on the text content. Longer messages or messages with Unicode characters split into multiple segments (concatenated on the recipient's device) and billed accordingly. See Plivo's Encoding and Concatenation guide.

Message Length Examples:

  • Standard SMS (GSM): "Hello, your code is 123456" = 28 chars = 1 segment
  • Unicode SMS: "Hello 👋 your code is 123456" = 30 chars = 1 segment (70 char limit)
  • Long SMS (GSM): 320 characters = 3 segments (153 chars each due to concatenation headers)

Sender ID Rules: US/Canada requires a Plivo number. Other countries have varying rules for numeric vs. alphanumeric sender IDs, often requiring pre-registration. Using an invalid or unregistered sender ID will cause message delivery failure. Check Plivo's country-specific SMS guidelines.

Trial Account Limitations: Sending is restricted to verified "Sandbox" numbers.

Opt-Out Handling: Regulations (like TCPA in the US) require handling STOP and HELP keywords. Plivo can manage standard opt-outs automatically for long codes and Toll-Free numbers if you configure this in the console. Ensure compliance with local regulations.

Compliance Requirements by Region:

  • US (TCPA): Obtain prior express written consent, honor opt-outs, register for 10DLC
  • EU (GDPR): Obtain explicit consent, maintain opt-out records, provide data access
  • Canada (CASL): Document consent, include contact info, honor unsubscribe requests
  • Australia (SPAM Act): Obtain consent, identify sender, provide opt-out mechanism

9. Performance Optimization for High-Volume SMS

For this simple single-message API, major optimizations are usually unnecessary. However, for higher volume:

  • Asynchronous Operations: Node.js and the Plivo SDK are already asynchronous, preventing the server from blocking during the API call. Using async/await maintains this benefit with cleaner syntax.
  • Connection Pooling: The Plivo SDK likely handles underlying HTTP connection management. No specific action needed here.
  • Payload Size: Keep request/response payloads minimal. Avoid sending excessively large JSON responses.
  • Queuing for High Volume: If you need to send many messages quickly (e.g., bulk notifications), don't call the Plivo API directly within the HTTP request handler for each message. Instead:
    1. Accept the request quickly.
    2. Push the message details (to, text) onto a message queue (like RabbitMQ, Redis Streams, AWS SQS, BullMQ).
    3. Have separate background worker processes read from the queue and make the calls to the Plivo API. This decouples the API from the sending process, improves response times, and allows for better rate control and retries.

Example BullMQ Queue Setup:

javascript
// Install: npm install bullmq ioredis

// queue.js
const { Queue } = require('bullmq');

const smsQueue = new Queue('sms-queue', {
  connection: {
    host: process.env.REDIS_HOST || 'localhost',
    port: process.env.REDIS_PORT || 6379,
  }
});

module.exports = smsQueue;

// In route handler (routes/sms.js):
const smsQueue = require('../queue');

router.post('/send-sms', async (req, res) => {
  const { to, text } = req.body;

  // Add to queue instead of sending immediately
  await smsQueue.add('send-sms', { to, text, from: SENDER_NUMBER });

  res.status(202).json({
    success: true,
    message: 'SMS queued for sending'
  });
});

// worker.js (separate process)
const { Worker } = require('bullmq');
const plivo = require('plivo');

const client = new plivo.Client(process.env.PLIVO_AUTH_ID, process.env.PLIVO_AUTH_TOKEN);

const worker = new Worker('sms-queue', async job => {
  const { to, text, from } = job.data;
  await client.messages.create({ src: from, dst: to, text });
}, {
  connection: {
    host: process.env.REDIS_HOST || 'localhost',
    port: process.env.REDIS_PORT || 6379,
  }
});
  • Caching: Not applicable for sending unique messages. Could be relevant if fetching contact info or templates frequently.
  • Load Testing: Use tools like k6, Artillery, or ApacheBench (ab) to simulate concurrent users and identify bottlenecks under load. Monitor CPU, memory, and response times during tests.

Example k6 Load Test:

javascript
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 10 }, // Ramp up to 10 users
    { duration: '1m', target: 10 },  // Stay at 10 users
    { duration: '30s', target: 0 },  // Ramp down
  ],
};

export default function () {
  const payload = JSON.stringify({
    to: '+14155552671',
    text: 'Load test message',
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const res = http.post('http://localhost:3000/api/send-sms', payload, params);

  check(res, {
    'status is 202': (r) => r.status === 202,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  sleep(1);
}
  • Profiling: Use Node.js built-in profiler (node --prof) or tools like Clinic.js to identify performance hotspots in your code.

10. Monitoring and Observability for SMS Applications

For production readiness:

  • Health Checks: Add a simple health check endpoint that verifies basic application status.

    javascript
    // Add to server.js
    app.get('/health', (req, res) => {
      // Basic check: just confirm the server is running
      // More advanced: check DB connection, Plivo connectivity
      res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
    });
  • Performance Metrics: Monitor key metrics like request latency, request rate (RPM), error rates (4xx, 5xx), and resource usage (CPU, memory). Tools like Prometheus/Grafana, Datadog APM, or New Relic can track these.

Example Prometheus Metrics:

javascript
// Install: npm install prom-client

// metrics.js
const client = require('prom-client');

const register = new client.Registry();
client.collectDefaultMetrics({ register });

const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register]
});

const smsCounter = new client.Counter({
  name: 'sms_sent_total',
  help: 'Total number of SMS messages sent',
  labelNames: ['status'],
  registers: [register]
});

module.exports = { register, httpRequestDuration, smsCounter };

// In server.js:
const { register } = require('./metrics');

app.get('/metrics', async (req, res) => {
  res.setHeader('Content-Type', register.contentType);
  res.send(await register.metrics());
});
  • Error Tracking: Integrate an error tracking service like Sentry or Bugsnag.

Complete Sentry Integration:

javascript
// Install: npm install @sentry/node @sentry/tracing

// At the top of server.js, before other imports
const Sentry = require('@sentry/node');
const Tracing = require('@sentry/tracing');

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV || 'development',
  tracesSampleRate: 1.0,
  integrations: [
    new Sentry.Integrations.Http({ tracing: true }),
    new Tracing.Integrations.Express({ app }),
  ],
});

// Request handler must be first middleware
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

// All your routes go here

// Error handler must be before any other error middleware
app.use(Sentry.Handlers.errorHandler());
  • Logging Best Practices: For production, replace console.log with a structured logging library (Winston, Pino) that supports log levels, JSON formatting, and multiple transports (console, files, external services).
  • Application Monitoring: Use APM tools to track request flows, database queries, external API calls, and identify performance bottlenecks.
  • Analytics: Track SMS metrics like total sent, delivery success rate, failure reasons, and message costs to understand usage patterns and optimize your implementation.

Deploying Your SMS Application to Production

Environment Configuration: Use environment-specific .env files or platform configuration (Heroku Config Vars, AWS Parameter Store, etc.) to manage credentials and settings across development, staging, and production.

Process Management: Use a process manager like PM2 or systemd to keep your Node.js application running, handle crashes with automatic restarts, and manage multiple instances for load balancing.

Example PM2 Configuration:

javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'plivo-sms-api',
    script: './server.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
  }]
};

// Start: pm2 start ecosystem.config.js
// Monitor: pm2 monit
// Logs: pm2 logs

Load Balancing: Deploy multiple instances of your application behind a load balancer (NGINX, AWS ALB, etc.) to distribute traffic and ensure high availability.

SSL/TLS: Always use HTTPS in production. Obtain SSL certificates from Let's Encrypt or your cloud provider.

Security Hardening: Review OWASP Top 10 vulnerabilities, implement proper input validation, use security headers (helmet), enable rate limiting, and keep dependencies updated.

Monitoring and Alerting: Set up alerts for critical metrics like high error rates, slow response times, or service downtime. Use tools like PagerDuty, Opsgenie, or cloud provider alerting.

Backup and Disaster Recovery: If you add a database, implement regular backups and test your disaster recovery procedures.

Cost Optimization: Monitor your Plivo usage and costs. Implement message queuing for batch sends to optimize throughput. Consider caching frequently accessed data to reduce API calls.

Platform-Specific Deployment:

Heroku:

bash
# Install Heroku CLI, then:
heroku create plivo-sms-api
heroku config:set PLIVO_AUTH_ID=xxx PLIVO_AUTH_TOKEN=xxx PLIVO_SENDER_NUMBER=+1xxx
git push heroku main

AWS Elastic Beanstalk:

bash
# Install EB CLI, then:
eb init plivo-sms-api --platform node.js --region us-east-1
eb create production
eb setenv PLIVO_AUTH_ID=xxx PLIVO_AUTH_TOKEN=xxx PLIVO_SENDER_NUMBER=+1xxx
eb deploy

Docker:

dockerfile
# Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

# Build: docker build -t plivo-sms-api .
# Run: docker run -p 3000:3000 --env-file .env plivo-sms-api

This comprehensive guide provides everything you need to send SMS messages with Plivo, Node.js, and Express. Build upon these patterns to create more sophisticated messaging features tailored to your application's needs.

Frequently Asked Questions

How to send SMS messages with Node.js and Express?

Use the Plivo API and Node.js SDK within an Express app. Create a POST /send-sms endpoint that accepts recipient number and message text, then uses the Plivo SDK to send the SMS via your Plivo account.

What is Plivo used for in this Node.js application?

Plivo is a cloud communications platform that provides the SMS sending functionality. The Node.js app interacts with Plivo's API using the Plivo Node.js SDK, abstracting away low-level API details.

Why does the Node.js app need Express.js?

Express.js simplifies the process of setting up routes and handling HTTP requests in the Node.js application. It provides a straightforward way to create the /send-sms API endpoint.

When should I use a Plivo number versus a Sender ID for SMS?

For sending SMS in the US or Canada, you must purchase a Plivo phone number. For other countries, you might be able to use a registered Alphanumeric Sender ID, but check Plivo's guidelines as requirements vary by country.

Can I send SMS to any number with a Plivo trial account?

Trial accounts typically restrict sending to verified "Sandbox" numbers within your Plivo console. You'll need a full Plivo account to send to regular numbers.

How to set up a Node.js project for sending SMS with Plivo?

Initialize a Node.js project using npm init, then install the 'express', 'plivo', and 'dotenv' packages. Create server.js for the main application logic, and a routes/sms.js file for the API endpoint handler.

What is the role of the .env file when using Plivo?

The .env file stores sensitive credentials like your Plivo Auth ID, Auth Token, and Sender Number. The 'dotenv' package loads these into process.env, making them accessible to your application.

How do I handle errors when sending SMS with the Plivo API?

Implement error handling for invalid inputs, missing credentials, and Plivo API errors. Use try...catch blocks to manage Plivo API calls, logging errors and sending appropriate responses to the client.

What format should the recipient phone number (\"to\") be in?

Use the E.164 international number format, which includes a '+' prefix followed by the country code and phone number (e.g., +12025550100).

How to secure a Node.js Express API for sending SMS via Plivo?

Use environment variables (.env) for credentials. Implement robust input validation and consider rate limiting using a library like 'express-rate-limit' and security headers with 'helmet'.

How to send SMS messages to multiple recipients using Node and Plivo?

The example code sends to one recipient per request. For multiple recipients, loop through the recipient list in your route handler and call client.messages.create for each, or use Plivo's bulk messaging API.

What should I do if the Plivo API request fails?

Check the error details in the Plivo API response and logs. Retry the request for transient errors (e.g. network or 5xx Plivo errors) using a retry mechanism with exponential backoff.

What data should be logged when using Plivo?

Log incoming request details, attempts to send SMS, Plivo responses (including 'messageUuid'), and detailed errors for debugging and auditing purposes. Use a logging library for production.

How to test the Plivo SMS API integration?

Use 'curl' or Postman to send requests to your local server or deployed API. Test both valid requests and error scenarios, including missing inputs, bad formats, and invalid credentials.