code examples

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

Build Bulk SMS Marketing Campaigns with Plivo, Node.js, and Express.js

Complete guide to building an Express.js REST API for sending bulk SMS marketing campaigns using Plivo. Covers 10DLC registration, TCPA 2025 compliance, concurrent messaging, error handling, and production security best practices.

⚠️ IMPORTANT FILENAME NOTICE: This article file is named "plivo-node-js-next-js-nextauth-marketing-campaigns.md" but contains Express.js content, NOT Next.js or NextAuth.js content. The guide implements a traditional Express.js REST API server. If you need Next.js App Router with NextAuth v5 (Auth.js) integration, you'll need substantial modifications including Next.js Route Handlers, server actions, and NextAuth v5 configuration. This notice will remain until the filename or content is corrected.


Build Bulk SMS Marketing Campaigns with Plivo, Node.js, and Express.js

This guide provides a complete walkthrough for building a backend system that sends bulk SMS marketing messages using Node.js, Express, and the Plivo Communications API. You'll learn everything from initial project setup to deployment considerations, focusing on creating a robust and scalable foundation.

By the end of this tutorial, you'll have a functional Express application with an API endpoint that accepts a list of phone numbers and a message body, then uses Plivo to send SMS messages concurrently for efficient campaign execution. This solves the common need for businesses to programmatically reach their audience via SMS for marketing, notifications, or alerts.

<!-- DEPTH: Introduction lacks concrete success metrics and ROI context (Priority: Medium) --> <!-- GAP: Missing real-world use case examples and business context (Type: Substantive) -->

Project Overview and Goals

Goal: Create a simple yet robust Node.js service that sends SMS messages in bulk via an API endpoint, leveraging Plivo for SMS delivery.

Problem Solved: Automates the process of sending marketing or notification SMS messages to a list of recipients, handling concurrency and basic error logging.

Technologies:

  • Node.js: A JavaScript runtime environment ideal for building scalable, non-blocking I/O applications like API servers. Required: Node.js v20 LTS "Iron" (Maintenance LTS through April 2026) or v22 LTS "Jod" (Active LTS through October 2025, Maintenance LTS through April 2027). Node.js v18 reached End of Life on April 30, 2025.
  • Express.js: A minimal and flexible Node.js web application framework providing robust features for web and mobile applications (API routing, middleware).
  • Plivo: A cloud communications platform providing APIs for SMS, Voice, and more. You'll use their Node.js SDK.
  • dotenv: A zero-dependency module that loads environment variables from a .env file into process.env. Essential for managing sensitive credentials.

System Architecture:

The system follows this basic flow:

  1. A Client/Frontend sends a POST request to the /api/campaigns endpoint of the Node.js/Express App.
  2. The Node.js/Express App processes the request and sends individual SMS requests to the Plivo SMS API.
  3. The Plivo SMS API handles the delivery of SMS messages to End User Mobiles.
  4. The Node.js/Express App logs the results of the send attempts (to the console or a log file).
<!-- EXPAND: Architecture section could benefit from a visual diagram and scalability discussion (Type: Enhancement) --> <!-- GAP: Missing information about concurrent request limits and throughput considerations (Type: Substantive) -->

Expected Outcome:

  • A running Express application.
  • An API endpoint (POST /api/campaigns) that accepts JSON payload: { "recipients": ["+1…", "+1…"], "message": "Your message here" }.
  • Successful dispatch of SMS messages to the provided recipients via Plivo.
  • Basic logging of successful sends and errors.

Prerequisites:

  • A Plivo Account: Sign up here.
  • Node.js v20 LTS or v22 LTS and npm (or yarn) installed locally. Download Node.js. Important: Node.js v18 reached End of Life on April 30, 2025 and no longer receives security updates.
  • Basic understanding of JavaScript, Node.js, and REST APIs.
  • A text editor or IDE (like VS Code).
  • Access to a command line/terminal.
  • 10DLC Registration Complete (for US SMS): If you're sending SMS to US recipients, complete A2P 10DLC registration with The Campaign Registry (TCR) through your Plivo account. Unregistered traffic incurs carrier surcharges ($0.0080 – $0.0100 per message) and may be blocked entirely by carriers. See Plivo's 10DLC documentation.
  • ngrok (Optional, but recommended for testing incoming messages if needed later, e.g., for handling replies or delivery reports via webhooks, which is beyond the scope of this initial guide): Install ngrok.
<!-- GAP: Prerequisites missing estimated time commitment and skill level details (Type: Substantive) -->

Understanding SMS Marketing Compliance (2025 Requirements)

Before implementing your SMS marketing system, you must understand and comply with current regulations. Non-compliance can result in fines of $500–$1,500 per violation and class-action lawsuits.

<!-- DEPTH: Compliance section lacks international regulations beyond US TCPA (Priority: High) --> <!-- GAP: Missing GDPR, CASL (Canada), and other international compliance requirements (Type: Critical) -->

TCPA Compliance Requirements (Updated April 2025)

The Telephone Consumer Protection Act (TCPA) governs SMS marketing in the United States. New rules effective April 11, 2025 expanded compliance requirements:

1. Express Written Consent:

  • Obtain prior express written consent before sending marketing messages
  • Make consent clear, conspicuous, and separate from other terms
  • Include disclosure that the consumer will receive automated text messages
  • One-to-one consent requirement takes effect January 26, 2026 (pending legal challenges)
<!-- GAP: Missing concrete examples of compliant consent forms (Type: Substantive) --> <!-- EXPAND: Could benefit from sample consent language and checkbox examples (Type: Enhancement) -->

2. Expanded Opt-Out Recognition:

  • Honor multiple opt-out formats beyond "STOP": "UNSUBSCRIBE", "CANCEL", "END", "QUIT", "Leave me alone"
  • Accept opt-outs via text, email, voicemail, or informal messages
  • Process text-based opt-outs immediately (you may send one confirmation within 5 minutes)
  • Honor other opt-out methods within 10 business days maximum

3. Time Restrictions:

  • Send messages only between 8:00 AM – 9:00 PM in the recipient's local time
  • Implement timezone detection for multi-region campaigns
<!-- GAP: Missing practical implementation guidance for timezone detection (Type: Substantive) --> <!-- DEPTH: Time restrictions lack code examples or library recommendations (Priority: Medium) -->

4. Required Documentation:

  • SMS Terms & Conditions page or section clearly describing:
  • Message frequency expectations
  • Message & data rates disclosure
  • Opt-out instructions (keywords like STOP, HELP)
  • Customer support contact information
  • SMS Privacy Policy section covering:
  • How you collect and store phone numbers
  • What types of messages you'll send
  • Data retention and security practices
  • Third-party sharing policies
<!-- EXPAND: Could benefit from template examples or links to compliant policy templates (Type: Enhancement) -->

10DLC Registration Requirements

A2P 10DLC (Application-to-Person 10-Digit Long Code) registration is mandatory for all businesses sending SMS at scale to US recipients:

What You Must Do:

  1. Register Your Brand with The Campaign Registry (TCR) via Plivo Console
  2. Register Your Campaign(s) describing your messaging use case (e.g., "Marketing Promotions", "Account Notifications")
  3. Provide Business Verification including:
    • Legal business name and EIN (Employer Identification Number)
    • Business website with compliant SMS terms/privacy policy
    • Industry classification
  4. Pay Registration Fees: One-time brand registration ($4) + recurring campaign fees ($10/month per campaign)
<!-- GAP: Missing step-by-step screenshots or visual walkthrough of registration process (Type: Substantive) --> <!-- DEPTH: Registration section lacks troubleshooting guidance for common rejection reasons (Priority: Medium) -->

Consequences of Non-Registration:

  • Carrier surcharges: $0.0080 – $0.0100 per message (AT&T, T-Mobile, Verizon)
  • Message blocking or filtering by carriers
  • Reduced throughput limits (3 messages/second vs. 60+ when registered)

Registration Timeline: Allow 1 – 2 weeks for approval. Start this process before implementing your system.

See Plivo's 10DLC Registration Guide for complete instructions.

1. Setting up the 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-sms-campaign
    cd plivo-sms-campaign
  2. Initialize Node.js Project: Create a package.json file to manage project dependencies and scripts.

    bash
    npm init -y

    (The -y flag accepts the default settings.)

  3. Install Dependencies: Install Express for the web server, Plivo's Node.js SDK to interact with their API, and dotenv for managing environment variables.

    bash
    npm install express plivo dotenv

    (Note: Modern Express versions include body-parsing capabilities, so you don't need body-parser separately).

  4. Create Project Structure: A good structure helps maintainability. Create some basic directories and files.

    bash
    # On macOS/Linux
    mkdir src config routes services
    touch src/app.js src/server.js config/plivo.js routes/campaign.js services/smsService.js .env .gitignore
    
    # On Windows (Command Prompt)
    mkdir src config routes services
    echo. > src/app.js
    echo. > src/server.js
    echo. > config/plivo.js
    echo. > routes/campaign.js
    echo. > services/smsService.js
    echo. > .env
    echo. > .gitignore
    
    # On Windows (PowerShell)
    mkdir src, config, routes, services
    New-Item src/app.js, src/server.js, config/plivo.js, routes/campaign.js, services/smsService.js, .env, .gitignore -ItemType File
    • src/: Contains your main application logic.
    • config/: For configuration files (like Plivo client setup).
    • routes/: Defines API endpoints.
    • services/: Contains business logic (like sending SMS).
    • .env: Stores environment variables (API keys, etc.). Never commit this file to Git.
    • .gitignore: Specifies intentionally untracked files that Git should ignore.
<!-- DEPTH: Project structure section lacks justification for architectural decisions (Priority: Low) -->
  1. Configure .gitignore: Add node_modules and .env to your .gitignore file to prevent committing dependencies and sensitive credentials.

    Code
    # Dependencies
    node_modules/
    
    # Environment variables
    .env
    
    # Logs
    logs
    *.log
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    
    # Optional files
    .DS_Store
  2. Set up Environment Variables (.env): You need your Plivo Auth ID, Auth Token, and a Plivo phone number (or Sender ID where applicable).

    • Find Credentials: Log in to your Plivo Console. Your Auth ID and Auth Token appear prominently on the dashboard.
    • Get a Plivo Number: Navigate to Phone NumbersBuy Numbers. Search for and purchase an SMS-enabled number suitable for your region (required for sending to US/Canada). If you're sending outside US/Canada, you might use an Alphanumeric Sender ID (see Caveats section).
    • Populate .env: Open the .env file and add your credentials:
    Code
    # Plivo Credentials
    PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
    PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
    PLIVO_SENDER_ID=YOUR_PLIVO_PHONE_NUMBER_OR_SENDER_ID # e.g., +14155551234
    
    # Server Configuration
    PORT=3000

    Replace the placeholder values with your actual credentials and number.

    Why dotenv? It keeps sensitive information like API keys out of your codebase, making it more secure and easier to manage different configurations for development, staging, and production environments.

<!-- GAP: Missing guidance on environment variable naming conventions and best practices (Type: Substantive) -->

2. Implementing Core Functionality

Implement the core logic for sending SMS messages via Plivo.

  1. Configure Plivo Client: Centralize the Plivo client initialization.

    javascript
    // config/plivo.js
    // Note: dotenv.config() should be called once at application entry point (e.g., app.js)
    const plivo = require('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 not found in environment variables.'
      );
      console.error('Ensure PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN are set in .env and dotenv.config() is called at app start.');
      process.exit(1); // Exit if credentials aren't set
    }
    
    const client = new plivo.Client(authId, authToken);
    
    module.exports = client;

    Why centralize? This ensures you initialize the client only once and makes it easily accessible throughout the application via require('../config/plivo').

  2. Create SMS Sending Service: This service contains the function responsible for sending a single SMS and the bulk sending logic.

    javascript
    // services/smsService.js
    // Note: dotenv.config() should be called once at application entry point (e.g., app.js)
    const plivoClient = require('../config/plivo');
    
    const senderId = process.env.PLIVO_SENDER_ID;
    
    if (!senderId) {
      console.error('PLIVO_SENDER_ID not found in environment variables. Ensure it is set in .env and dotenv.config() is called at app start.');
      process.exit(1);
    }
    
    /**
     * Sends a single SMS message using Plivo.
     * @param {string} recipient - The destination phone number in E.164 format (e.g., +12223334444).
     * @param {string} message - The text message body.
     * @returns {Promise<object>} - A promise resolving with the Plivo API response on success.
     * @throws {Error} - Throws an error if the SMS sending fails.
     */
    const sendSingleSms = async (recipient, message) => {
      console.log(`Attempting to send SMS to ${recipient}`);
      try {
        const response = await plivoClient.messages.create(
          senderId, // src
          recipient, // dst
          message // text
        );
        console.log(`SMS sent successfully to ${recipient}:`, response.messageUuid);
        return response; // Contains message_uuid, api_id, etc.
      } catch (error) {
        console.error(`Failed to send SMS to ${recipient}:`, error);
        // Re-throw the error to be handled by the caller (e.g., the API route)
        // You might want more specific error handling/logging here in production
        throw new Error(`Plivo API error sending to ${recipient}: ${error.message}`);
      }
    };
    
    /**
     * Sends SMS messages to multiple recipients concurrently.
     * @param {string[]} recipients - An array of destination phone numbers in E.164 format.
     * @param {string} message - The text message body.
     * @returns {Promise<object>} - A promise resolving with the results of all send attempts.
     */
    const sendBulkSms = async (recipients, message) => {
      if (!Array.isArray(recipients) || recipients.length === 0) {
        throw new Error('Recipients array cannot be empty.');
      }
    
      // Create an array of promises, one for each SMS send operation
      // Each promise resolves to an object indicating success or failure
      const sendPromises = recipients.map(recipient =>
        sendSingleSms(recipient, message)
          .then(response => ({ // Wrap success
            status: 'success',
            value: response,
            recipient: recipient
          }))
          .catch(error => ({ // Wrap failure
            status: 'failed',
            recipient: recipient,
            error: error.message,
          }))
      );
    
      // Use Promise.allSettled to wait for all promises to resolve or reject
      // This ensures you process all numbers even if some fail
      const results = await Promise.allSettled(sendPromises);
    
      // Process results to provide a summary
      const report = {
        totalAttempted: recipients.length,
        successCount: 0,
        failedCount: 0,
        details: [],
      };
    
      results.forEach((result, index) => {
          const recipient = recipients[index]; // Get recipient from original array
    
          // Since we used .catch within map, all promises should settle as 'fulfilled'
          if (result.status === 'fulfilled') {
              const outcome = result.value; // This is our { status: 'success', … } or { status: 'failed', … } object
    
              if (outcome.status === 'success') {
                  report.successCount++;
                  report.details.push({
                      recipient: recipient,
                      status: 'success',
                      // Safely access messageUuid from the Plivo response within outcome.value
                      // Plivo SDK might return messageUuid in an array, hence ?.[] access
                      messageUuid: outcome.value?.messageUuid?.[0] || 'N/A',
                  });
              } else { // outcome.status === 'failed'
                  report.failedCount++;
                  report.details.push({
                      recipient: recipient,
                      status: 'failed',
                      error: outcome.error || 'Unknown failure reason',
                  });
              }
          } else {
              // This block should ideally not be reached due to the inner .catch,
              // but handle defensively in case of unexpected promise rejection.
              report.failedCount++;
              report.details.push({
                  recipient: recipient,
                  status: 'failed',
                  error: result.reason?.message || 'Promise rejected unexpectedly',
              });
          }
      });
    
      console.log(`Bulk send report: ${report.successCount} succeeded, ${report.failedCount} failed.`);
      return report;
    };
    
    module.exports = {
      sendSingleSms,
      sendBulkSms,
    };

    Why Promise.allSettled? For bulk operations, you often want to attempt all actions even if some fail. Promise.all would reject immediately on the first error. Promise.allSettled waits for all promises to either fulfill or reject, giving you a complete picture of the outcomes. Map the results to provide a clear success/failure report. Why async/await? It makes asynchronous code (like API calls) look and behave a bit more like synchronous code, improving readability compared to nested .then() calls.

<!-- GAP: Missing discussion of message batching strategies for very large campaigns (Type: Substantive) --> <!-- DEPTH: Concurrency control and rate limiting implementation missing (Priority: High) --> <!-- EXPAND: Could benefit from performance benchmarks and optimization tips (Type: Enhancement) -->

3. Building the API Layer

Create the Express route that receives campaign requests and triggers the SMS service.

  1. Create Campaign Route: This file defines the endpoint for initiating SMS campaigns.

    javascript
    // routes/campaign.js
    const express = require('express');
    const { sendBulkSms } = require('../services/smsService');
    
    const router = express.Router();
    
    // POST /api/campaigns
    router.post('/', async (req, res) => {
      const { recipients, message } = req.body;
    
      // --- Basic Input Validation ---
      if (!recipients || !Array.isArray(recipients) || recipients.length === 0) {
        return res.status(400).json({ error: 'Invalid or missing "recipients" array.' });
      }
      if (!message || typeof message !== 'string' || message.trim() === '') {
        return res.status(400).json({ error: 'Invalid or missing "message" string.' });
      }
      // Add more specific validation (e.g., E.164 format check) if needed
    
      try {
        console.log(`Received campaign request: ${recipients.length} recipients.`);
        // Call the bulk sending service
        const report = await sendBulkSms(recipients, message);
    
        // Respond with the report
        res.status(200).json({
          message: `Campaign processing initiated. Results below.`,
          report: report,
        });
      } catch (error) {
        console.error('Error processing campaign request:', error);
        // Generic error for the client, specific error logged server-side
        res.status(500).json({ error: 'Failed to process SMS campaign.', details: error.message });
      }
    });
    
    module.exports = router;
    • Destructure recipients and message from the request body (req.body).
    • Basic validation checks if the required fields exist and are of the correct type. More robust validation (e.g., using libraries like joi or express-validator) is recommended for production.
    • Call the sendBulkSms service function.
    • The response includes the detailed report the service generates.
<!-- GAP: Missing API documentation examples (OpenAPI/Swagger spec) (Type: Substantive) --> <!-- DEPTH: Validation section needs more comprehensive examples of edge cases (Priority: Medium) -->
  1. Set up Express App: Configure the main Express application to use middleware and mount the router.

    javascript
    // src/app.js
    require('dotenv').config(); // Ensure env vars are loaded ONCE at the very start
    const express = require('express');
    const campaignRoutes = require('../routes/campaign');
    
    const app = express();
    
    // --- Middleware ---
    // Use Express's built-in JSON body parser
    app.use(express.json());
    // Use Express's built-in URL-encoded body parser (optional, for form data)
    app.use(express.urlencoded({ extended: true }));
    
    // Basic Logging Middleware (Example)
    app.use((req, res, next) => {
      console.log(`${new Date().toISOString()}${req.method} ${req.path}`);
      next(); // Pass control to the next middleware/route handler
    });
    
    // --- Routes ---
    app.get('/health', (req, res) => {
      res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
    });
    
    // Mount the campaign routes under the /api/campaigns prefix
    app.use('/api/campaigns', campaignRoutes);
    
    // --- Basic Error Handling Middleware (Catch-all) ---
    // This should be defined AFTER your routes
    app.use((err, req, res, next) => {
      console.error('Unhandled Error:', err.stack || err);
      res.status(500).json({ error: 'Something went wrong on the server!' });
    });
    
    module.exports = app; // Export the configured app
    • Import express and your campaign router.
    • express.json() middleware is crucial for parsing the JSON payload sent to your API.
    • A simple logging middleware shows incoming requests.
    • A /health endpoint is added for basic monitoring.
    • Mount the campaign router at /api/campaigns.
    • Include a basic catch-all error handler.
  2. Create Server Entry Point: This file starts the actual HTTP server.

    javascript
    // src/server.js
    const app = require('./app'); // Import the configured Express app
    
    const PORT = process.env.PORT || 3000; // Use port from .env or default to 3000
    
    app.listen(PORT, () => {
      console.log(`Server listening on port ${PORT}`);
      console.log(`Campaign endpoint available at: POST http://localhost:${PORT}/api/campaigns`);
      console.log(`Health check available at: GET http://localhost:${PORT}/health`);
    });
  3. Add Start Script: Modify your package.json to add a convenient start script.

    json
    // package.json (add within the "scripts" object)
    "scripts": {
      "start": "node src/server.js",
      "test": "echo \"Error: no test specified\" && exit 1"
    },
  4. Run the Application:

    bash
    npm start

    You should see output indicating the server is running on port 3000.

<!-- GAP: Missing testing instructions and sample curl/Postman requests (Type: Critical) --> <!-- EXPAND: Could benefit from troubleshooting common startup issues (Type: Enhancement) -->

4. Integrating with Plivo (Deep Dive)

You've already set up the client, but here are the key integration points:

  • Credentials: PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN are mandatory. They authenticate your application with Plivo's API.
    • Location: Plivo Console Dashboard (console.plivo.com).
    • Security: Store securely in .env and access via process.env. Never hardcode them in your source code. Ensure dotenv.config() is called once at your application's entry point (src/app.js in this case).
  • Sender ID (PLIVO_SENDER_ID): This is the "From" number or ID shown to the recipient.
    • Format: E.164 format phone number (e.g., +14155551234) is required for sending to US/Canada. Must be a Plivo number you own/rented.
    • Location: Find/Buy numbers in Plivo Console → Phone Numbers → Your Numbers / Buy Numbers.
    • Alternatives: For other countries, you might be able to use a registered Alphanumeric Sender ID (e.g., "MyBrand"). See the "Caveats" section. Check Plivo's SMS API Coverage page for country-specific rules.
    • Configuration: Set in .env as PLIVO_SENDER_ID.
<!-- GAP: Missing detailed explanation of Plivo API response structure and status codes (Type: Substantive) --> <!-- DEPTH: Webhook integration for delivery reports not covered (Priority: Medium) --> <!-- EXPAND: Could benefit from examples of advanced Plivo features (MMS, scheduled sends) (Type: Enhancement) -->

5. Error Handling, Logging, and Retry Mechanisms

Our current setup includes basic error handling and logging. Let's refine it.

  • Consistent Error Handling:
    • The smsService catches errors during the Plivo API call (sendSingleSms) and logs them with recipient info.
    • sendBulkSms uses Promise.allSettled and compiles a detailed report, distinguishing successes from failures.
    • The API route (routes/campaign.js) catches errors during request processing (validation, service calls) and returns appropriate HTTP status codes (400 for bad requests, 500 for server errors).
    • The global error handler in app.js catches any unhandled exceptions.
  • Logging:
    • Currently using console.log and console.error. For production, use a dedicated logging library like winston or pino.

    • Setup (Example with Winston):

      bash
      npm install winston

      Create a logger configuration (e.g., config/logger.js):

      javascript
      // config/logger.js
      const winston = require('winston');
      
      const logger = winston.createLogger({
        level: 'info', // Log only info and above by default
        format: winston.format.combine(
          winston.format.timestamp(),
          winston.format.json() // Log in JSON format
        ),
        transports: [
          // - Write all logs with level `error` and below to `error.log`
          new winston.transports.File({ filename: 'error.log', level: 'error' }),
          // - Write all logs with level `info` and below to `combined.log`
          new winston.transports.File({ filename: 'combined.log' }),
        ],
      });
      
      // If we're not in production then log to the `console` with the format:
      // `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
      if (process.env.NODE_ENV !== 'production') {
        logger.add(new winston.transports.Console({
          format: winston.format.simple(),
        }));
      }
      
      module.exports = logger;

      Replace console.log/error calls with logger.info(), logger.warn(), logger.error().

    • Log Analysis: JSON logs are easier to parse by log management systems (like Datadog, Splunk, ELK stack). Log unique identifiers (like messageUuid from Plivo) to trace requests.

<!-- GAP: Missing guidance on log retention policies and compliance requirements (Type: Substantive) --> <!-- DEPTH: Structured logging examples and best practices incomplete (Priority: Medium) -->
  • Retry Mechanisms:
    • Plivo handles some level of retries internally for deliverability.

    • For transient network errors or specific Plivo API errors (e.g., rate limits - 429 Too Many Requests), you might implement application-level retries.

    • Strategy: Use exponential backoff (wait longer between retries).

    • Example Concept (in sendSingleSms):

      javascript
      // Inside sendSingleSms, conceptual example - use a library for robustness
      const maxRetries = 3;
      let attempt = 0;
      while (attempt < maxRetries) {
        try {
          const response = await plivoClient.messages.create(/*...*/);
          // Success, break loop
          return response;
        } catch (error) {
          attempt++;
          // Example: Retry only on specific, potentially transient errors like rate limits
          if (error.statusCode === 429 && attempt < maxRetries) {
            const delay = Math.pow(2, attempt) * 100; // Exponential backoff (100ms base)
            console.warn(`Rate limited sending to ${recipient}. Retrying attempt ${attempt}/${maxRetries} after ${delay}ms...`);
            await new Promise(resolve => setTimeout(resolve, delay));
          } else {
            // Don't retry other errors or if max retries reached
            console.error(`Failed to send SMS to ${recipient} after ${attempt} attempts:`, error);
            // Re-throw the original error or a new one summarizing the failure
            throw new Error(`Plivo API error sending to ${recipient}: ${error.message}`);
          }
        }
      }
      // If loop finishes without returning/throwing, it means max retries failed
      throw new Error(`Failed to send SMS to ${recipient} after ${maxRetries} attempts.`);
    • Recommendation: While the above illustrates the concept, using established libraries like async-retry or p-retry is strongly recommended for production code. They handle edge cases and configuration more robustly.

    • Caution: Be careful not to retry non-recoverable errors (e.g., invalid number 400, insufficient funds 402). Check Plivo's API error codes and retry logic documentation. Only retry errors that are likely temporary.

<!-- GAP: Missing circuit breaker pattern discussion for API resilience (Type: Substantive) --> <!-- DEPTH: Error classification and handling strategies need more detail (Priority: Medium) -->

6. Database Schema and Data Layer (Conceptual)

While this guide focuses on sending, a real marketing platform needs data persistence.

  • Need: Store contacts/subscribers, campaign details (message, target list, schedule), send status (messageUuid, delivered, failed), and potentially track responses or unsubscribes.

  • Schema Example (Conceptual - e.g., using PostgreSQL):

    sql
    CREATE TABLE contacts (
        contact_id SERIAL PRIMARY KEY,
        phone_number VARCHAR(20) UNIQUE NOT NULL, -- E.164 format
        first_name VARCHAR(100),
        last_name VARCHAR(100),
        subscribed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
        is_active BOOLEAN DEFAULT TRUE
        -- Add other relevant fields (tags, groups, etc.)
    );
    
    CREATE TABLE campaigns (
        campaign_id SERIAL PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        message_body TEXT NOT NULL,
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
        scheduled_at TIMESTAMP WITH TIME ZONE,
        status VARCHAR(20) DEFAULT 'draft' -- e.g., draft, sending, completed, failed
    );
    
    CREATE TABLE campaign_sends (
        send_id SERIAL PRIMARY KEY,
        campaign_id INTEGER REFERENCES campaigns(campaign_id) ON DELETE CASCADE,
        contact_id INTEGER REFERENCES contacts(contact_id) ON DELETE CASCADE,
        plivo_message_uuid VARCHAR(50) UNIQUE, -- From Plivo response
        status VARCHAR(20) DEFAULT 'queued', -- e.g., queued, sent, delivered, failed, undelivered
        status_updated_at TIMESTAMP WITH TIME ZONE,
        error_message TEXT,
        sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    );
    
    -- Index for faster lookups
    CREATE INDEX idx_campaign_sends_status ON campaign_sends(status);
    CREATE INDEX idx_campaign_sends_uuid ON campaign_sends(plivo_message_uuid);

    (An Entity Relationship Diagram would visually represent these relationships).

<!-- GAP: Missing opt-out/unsubscribe tracking table and TCPA compliance fields (Type: Critical) --> <!-- DEPTH: Database schema lacks audit trail and GDPR data retention fields (Priority: High) --> <!-- EXPAND: Could benefit from migration scripts and ORM setup examples (Type: Enhancement) -->
  • Data Access: Use an ORM (like Sequelize, Prisma, TypeORM) or a query builder (like Knex.js) to interact with the database from Node.js. This involves setting up database connections, defining models/schemas in code, and writing queries/mutations.
  • Implementation:
    1. Choose a database and ORM/query builder.
    2. Install necessary packages (pg, sequelize, etc.).
    3. Configure database connection (likely using .env).
    4. Define models matching the schema.
    5. Implement functions to:
      • Fetch contacts for a campaign.
      • Create a campaign record.
      • Insert campaign_sends records before sending.
      • Update campaign_sends with plivo_message_uuid and status after sending attempts.
      • (Advanced) Set up a webhook to receive delivery reports from Plivo and update campaign_sends status (delivered, failed, etc.).
<!-- GAP: Missing concrete ORM implementation examples with actual code (Type: Substantive) -->

7. Adding Security Features

Security is paramount, especially when handling user data and API keys.

  • Input Validation:
    • Already implemented basic checks in routes/campaign.js.

    • Enhancement: Use robust libraries like joi or express-validator for complex validation (e.g., ensuring phone numbers match E.164 format, checking message length).

    • Example (using express-validator):

      bash
      npm install express-validator
      javascript
      // routes/campaign.js (modified)
      const express = require('express');
      const { sendBulkSms } = require('../services/smsService');
      const { body, validationResult } = require('express-validator');
      
      const router = express.Router();
      
      router.post('/',
        // Validation middleware chain
        body('recipients').isArray({ min: 1 }).withMessage('Recipients must be a non-empty array.'),
        // Example E.164 validation - adjust options as needed for international numbers
        body('recipients.*').isMobilePhone('any', { strictMode: true })
            .withMessage('Each recipient must be a valid phone number, preferably in E.164 format (e.g., +14155551234).')
            .custom((value) => {
                // Add explicit '+' check for E.164 enforcement if needed
                if (!value.startsWith('+')) {
                    throw new Error('Recipient phone number must start with + (E.164 format).');
                }
                return true;
            }),
        body('message').isString().trim().notEmpty().withMessage('Message must be a non-empty string.'),
      
        async (req, res) => {
          const errors = validationResult(req);
          if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
          }
      
          // Destructure validated data (req.body is already updated by middleware)
          const { recipients, message } = req.body;
      
          // ... rest of the handler logic using validated recipients and message ...
          try {
            console.log(`Received validated campaign request: ${recipients.length} recipients.`);
            const report = await sendBulkSms(recipients, message);
            res.status(200).json({
              message: `Campaign processing initiated. Results below.`,
              report: report,
            });
          } catch (error) {
            console.error('Error processing campaign request:', error);
            res.status(500).json({ error: 'Failed to process SMS campaign.', details: error.message });
          }
        }
      );
      
      module.exports = router;
  • Sanitization: While less critical for SMS text content itself (as Plivo handles encoding), sanitize any user input that might be stored in a database or displayed elsewhere to prevent XSS if data is ever rendered in HTML. Libraries like dompurify (if rendering) or simple replacements can help.
  • Authentication/Authorization: Our current API is open. In production, you'd need to protect it:
    • API Keys: Issue unique keys to clients. Verify the key in middleware.
    • JWT (JSON Web Tokens): For user-based authentication if a frontend is involved.
    • Basic Auth: Simpler, but less secure (credentials sent with each request).
<!-- GAP: Missing concrete authentication implementation examples (Type: Critical) --> <!-- DEPTH: Authorization section lacks role-based access control (RBAC) discussion (Priority: High) -->
  • Rate Limiting: Prevent abuse and protect your Plivo account balance/API limits.

    • Implementation: Use middleware like express-rate-limit.

      bash
      npm install express-rate-limit
      javascript
      // src/app.js (add in middleware section, before API routes)
      const rateLimit = require('express-rate-limit');
      
      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
      });
      
      // Apply to all API routes or specific ones
      app.use('/api/', apiLimiter);
  • Secure Credential Storage: We are using .env, which is good. Ensure the .env file has strict file permissions on the server and is never checked into version control. Use environment variables provided by your hosting platform for production.

  • Helmet: Use the helmet middleware for setting various security-related HTTP headers (like Content-Security-Policy, Strict-Transport-Security).

    bash
    npm install helmet
    javascript
    // src/app.js (add early in middleware section)
    const helmet = require('helmet');
    app.use(helmet());
<!-- GAP: Missing CORS configuration guidance for frontend integration (Type: Substantive) --> <!-- DEPTH: Security section lacks discussion of SQL injection prevention (Priority: High) --> <!-- EXPAND: Could benefit from security audit checklist and penetration testing guidance (Type: Enhancement) -->

8. Handling Special Cases

Real-world SMS involves nuances:

  • E.164 Format: Plivo (and most providers) require phone numbers in E.164 format (e.g., +14155551234). Your validation should enforce this (see express-validator example above).
  • Sender ID Regulations:
    • US/Canada: Must use a Plivo-rented, SMS-enabled local or toll-free number. Alphanumeric Sender IDs are generally not supported for sending to these countries due to carrier restrictions and regulations like A2P 10DLC.
<!-- GAP: Missing comprehensive international sender ID regulations by country (Type: Substantive) --> <!-- DEPTH: Special cases section incomplete - needs MMS, Unicode, message length limits (Priority: High) --> <!-- EXPAND: Could benefit from examples handling special characters and emoji (Type: Enhancement) -->

Frequently Asked Questions (FAQ)

How do I send bulk SMS marketing campaigns with Plivo and Express.js?

Create an Express.js application with a /api/campaigns POST endpoint that accepts a recipients array and message string. Use Plivo's Node.js SDK with Promise.allSettled() to send SMS messages concurrently to multiple recipients. Install dependencies with npm install express plivo dotenv, configure your Plivo Auth ID and Auth Token in environment variables, and implement error handling to track successful and failed deliveries for each recipient in your campaign.

What is 10DLC registration and why is it required for SMS marketing?

A2P 10DLC (Application-to-Person 10-Digit Long Code) registration is mandatory for all businesses sending SMS at scale to US recipients. Register your brand and campaigns through The Campaign Registry (TCR) via your Plivo account. Without 10DLC registration, you'll face carrier surcharges of $0.0080 – $0.0100 per message, reduced throughput limits (3 messages/second vs. 60+ when registered), and potential message blocking by AT&T, T-Mobile, and Verizon. Allow 1 – 2 weeks for approval and budget ~$4 for brand registration plus ~$10/month per campaign.

What are the TCPA compliance requirements for SMS marketing in 2025?

New TCPA rules effective April 11, 2025 require: (1) Prior express written consent before sending marketing messages, (2) Expanded opt-out recognition beyond "STOP" including "UNSUBSCRIBE", "CANCEL", "END", "QUIT", and informal messages like "Leave me alone", (3) Processing text-based opt-outs immediately with max one confirmation within 5 minutes, (4) Honoring other opt-out methods within 10 business days, (5) Sending messages only between 8:00 AM – 9:00 PM recipient local time, and (6) Maintaining SMS Terms & Conditions and Privacy Policy pages. Non-compliance can result in fines of $500 – $1,500 per violation plus class-action lawsuits.

Which Node.js version should I use for a Plivo SMS application in 2025?

Use Node.js v20 LTS "Iron" (Maintenance LTS through April 2026) or v22 LTS "Jod" (Active LTS through October 2025, Maintenance LTS through April 2027). Node.js v18 reached End of Life on April 30, 2025 and no longer receives security updates. Node.js v22 is recommended for new projects as it provides Active LTS support throughout 2025 with the latest features and performance improvements.

How do I implement error handling for bulk SMS campaigns with Plivo?

Use Promise.allSettled() instead of Promise.all() to ensure all SMS send attempts complete even if some fail. Wrap each Plivo messages.create() call in a try-catch block, log recipient-specific errors with console.error(), and return a detailed report object containing totalAttempted, successCount, failedCount, and a details array with status for each recipient. For production applications, implement exponential backoff retry logic for transient errors like rate limits (HTTP 429), use Winston or Pino for structured logging, and avoid retrying non-recoverable errors like invalid numbers (HTTP 400) or insufficient funds (HTTP 402).

What is E.164 phone number format and why does Plivo require it?

E.164 is the international standard for phone number formatting: a plus sign (+) followed by country code and subscriber number with no spaces, hyphens, or parentheses (e.g., +14155551234 for US, +442071234567 for UK). Plivo requires E.164 format to correctly route SMS messages internationally and identify the destination carrier. Implement validation using express-validator with isMobilePhone('any', { strictMode: true }) and a custom validator to ensure numbers start with "+". Sending non-E.164 formatted numbers will result in API errors or message delivery failures.

How much does it cost to send SMS with Plivo?

Plivo offers pay-as-you-go pricing starting at $0.005 per message for receive SMS (free) and send SMS varies by destination country. US SMS typically costs $0.0075 – $0.0110 per message for registered 10DLC traffic. Unregistered traffic incurs additional carrier surcharges: $0.0080 (T-Mobile), $0.0100 (AT&T and Verizon). Volume discounts are available with committed spend agreements starting at $750/month with annual contracts. Check Plivo's SMS Pricing page for specific rates by country and 10DLC documentation for registration fees.

<!-- EXPAND: FAQ section could benefit from 5-10 additional common questions (Type: Enhancement) -->

How do I set up Express.js middleware for a Plivo SMS API?

Configure Express.js middleware in this order: (1) Call dotenv.config() first to load environment variables, (2) Use express.json() and express.urlencoded({ extended: true }) for parsing request bodies, (3) Add logging middleware to track incoming requests, (4) Implement express-rate-limit to prevent API abuse (e.g., 100 requests per 15 minutes per IP), (5) Add helmet() for security headers, (6) Mount your campaign routes with app.use('/api/campaigns', campaignRoutes), and (7) Add error handling middleware last to catch unhandled errors. Test the /health endpoint to verify middleware configuration before sending SMS.

What SMS opt-out keywords must I support for TCPA compliance?

Support these opt-out keywords (case-insensitive): STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, and QUIT. The 2025 TCPA rules also require honoring informal opt-out messages like "Leave me alone", "No more", "Remove me", plus opt-outs via email, voicemail, and other channels. Process text-based opt-outs immediately – you may send one confirmation message within 5 minutes stating the opt-out was successful, then never send marketing messages to that number again. Maintain an opt-out database table and check it before sending any campaign. Honor non-text opt-outs within 10 business days maximum.

<!-- GAP: Missing implementation example for automated opt-out processing (Type: Substantive) -->

How do I store Plivo credentials securely in Express.js?

Store Plivo Auth ID, Auth Token, and Sender ID in a .env file at your project root. Add .env to .gitignore to prevent committing credentials to version control. Load variables with dotenv.config() called once at your application entry point (src/app.js). Access credentials via process.env.PLIVO_AUTH_ID and process.env.PLIVO_AUTH_TOKEN. For production deployments, use your hosting platform's environment variable system (Heroku Config Vars, AWS Systems Manager Parameter Store, Docker secrets). Never hardcode credentials in source code. Set strict file permissions on .env (chmod 600) and implement credential rotation policies for production systems.

What database schema should I use for SMS marketing campaigns?

Implement three core tables: (1) contacts table with contact_id, phone_number (E.164 format, unique), first_name, last_name, subscribed_at, is_active, and optional tags/groups, (2) campaigns table with campaign_id, name, message_body, created_at, scheduled_at, status (draft/sending/completed/failed), and (3) campaign_sends table with send_id, campaign_id (foreign key), contact_id (foreign key), plivo_message_uuid, status (queued/sent/delivered/failed/undelivered), status_updated_at, error_message, sent_at. Add indexes on campaign_sends.status and campaign_sends.plivo_message_uuid for faster lookups. Use an ORM like Prisma, Sequelize, or TypeORM to manage relationships and implement webhook handlers to receive Plivo delivery reports and update campaign_sends status.

How long does 10DLC registration take with Plivo?

10DLC registration typically takes 1 – 2 weeks for approval after submitting complete documentation. The process involves: (1) Brand registration with The Campaign Registry (TCR) requiring your legal business name, EIN, business website with compliant SMS terms/privacy policy, and industry classification – usually approved within 1 – 3 business days, (2) Campaign registration describing your messaging use case (Marketing Promotions, Account Notifications, etc.) – typically approved within 1 – 7 business days, and (3) Carrier vetting which varies by carrier tier and trust score. Start the registration process before implementing your SMS system to avoid delays. High-volume businesses may qualify for Standard or Premium tiers with faster approval and higher throughput limits.

<!-- GAP: Missing deployment section covering production hosting options (Type: Critical) --> <!-- GAP: Missing monitoring and observability section for production systems (Type: Critical) --> <!-- GAP: Missing performance optimization and scaling strategies section (Type: Substantive) --> <!-- GAP: Missing cost optimization and budget management guidance (Type: Substantive) --> <!-- EXPAND: Could benefit from complete working example repository link (Type: Enhancement) -->

Frequently Asked Questions

How to send bulk SMS messages using Node.js?

Use Node.js with Express.js to create an API endpoint and integrate the Plivo Node.js SDK. This endpoint receives recipient numbers and the message, then uses Plivo to send the SMS messages concurrently. This setup is ideal for marketing campaigns, notifications, and alerts, automating the process of reaching your target audience.

What is Plivo used for in SMS marketing?

Plivo is a cloud communications platform that provides the necessary APIs for sending SMS messages. The Node.js SDK interacts with these APIs, making it possible to send individual or bulk SMS messages programmatically. It handles the complexities of message delivery and provides tools for managing your SMS campaigns.

Why does dotenv matter for API keys?

Dotenv is crucial for storing API keys and other credentials securely. It loads environment variables from a .env file, keeping sensitive information out of your codebase and version control. Ensure PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, and PLIVO_SENDER_ID are properly set in your .env file and loaded using dotenv.config() at application startup.

When should I use Promise.allSettled for SMS sending?

Promise.allSettled is essential when sending bulk SMS messages because it ensures that all send attempts are made, even if some fail. Unlike Promise.all, which rejects on the first error, Promise.allSettled waits for all promises to either fulfill or reject, providing a complete report of successes and failures. This is crucial for understanding the results of a bulk SMS campaign.

Can I use an alphanumeric sender ID with Plivo?

Alphanumeric Sender IDs might be possible for countries outside the US and Canada. However, for sending SMS to the US and Canada, you must use a Plivo-rented, SMS-enabled phone number due to regulations and carrier restrictions. Check Plivo's SMS API Coverage page for country-specific rules regarding sender IDs.

How to set up a Plivo SMS campaign endpoint?

Create a POST route in your Express app (e.g., /api/campaigns) that receives a JSON payload with an array of recipient phone numbers ("recipients") and the message body ("message"). The route handler should validate the input, call the SMS sending service, and return a report of the send attempts. This endpoint serves as the entry point for initiating your SMS campaigns.

What is the role of Express.js in this SMS setup?

Express.js provides the framework for creating the API endpoint that receives campaign requests. It handles routing, middleware (like body parsing for JSON payloads), and error handling. Express simplifies the process of building a web server and managing API interactions, making it the foundation of our SMS campaign application.

How to validate recipient phone numbers effectively?

For production applications, use dedicated validation libraries such as 'joi' or 'express-validator' to ensure phone numbers adhere to E.164 format. This helps prevent sending errors due to invalid recipient data. 'express-validator' offers specific validators like isMobilePhone('any', { strictMode: true }) which can further enhance your validation process.

What are good logging practices for an SMS application?

Use a structured logging library like Winston or Pino, logging in JSON format for easier analysis by log management systems. Log essential information like timestamps, request details, Plivo message UUIDs, and errors. This helps in debugging issues, tracking messages, and monitoring the health of your SMS service. Ensure sensitive information isn't logged.

Why is error handling important in SMS campaigns?

Robust error handling is essential to manage issues like network problems, invalid phone numbers, and Plivo API errors (like rate limiting). Implementing try-catch blocks, using Promise.allSettled, and potentially adding retry mechanisms with exponential backoff ensures that issues are handled gracefully without crashing the application and that you get a complete picture of campaign results.

What database schema should I use for storing SMS campaign data?

Consider a relational database schema with tables for contacts (phone numbers, subscription status), campaigns (message, schedule), and campaign_sends (linking campaigns to contacts and storing send status, Plivo message UUIDs). This structure facilitates tracking message delivery, managing contacts, and analyzing campaign performance.

How to implement rate limiting in Express.js?

Use the 'express-rate-limit' middleware to prevent API abuse and protect your Plivo account balance. This middleware lets you limit the number of requests from a specific IP address within a time window. It's crucial for preventing unexpected charges and maintaining the stability of your SMS service.

How to secure my Plivo credentials in a Node.js app?

Store your Plivo Auth ID, Auth Token, and sender ID in a .env file, ensuring this file has restricted permissions and is never committed to version control. Utilize dotenv.config() to load these variables into process.env at application start, keeping sensitive information separate from your codebase.

What's the importance of E.164 format for phone numbers?

Plivo requires phone numbers in E.164 format (e.g., +14155551234) for accurate message delivery. Ensure your application validates and normalizes phone numbers to this format, using validation libraries like 'express-validator' and its isMobilePhone validator, potentially adding a custom check for the leading '+' character, for improved reliability.

How to add retry logic for handling Plivo API errors?

Use libraries like 'async-retry' or 'p-retry' to implement application-level retries with exponential backoff for transient errors (e.g., network issues, rate limiting). Be mindful of Plivo's API error codes and retry only recoverable errors, avoiding retrying on invalid numbers or insufficient funds. This enhances resilience and improves message deliverability.