code examples

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

Plivo Node.js Bulk Broadcast Messaging

Build a reliable bulk SMS broadcasting application using Node.js, Express, and Plivo Communications Platform

Sending SMS messages individually is straightforward, but scaling to hundreds or thousands of recipients requires a more robust approach. Broadcasting messages efficiently minimizes API calls, reduces latency, and simplifies tracking.

This guide details how to build a reliable bulk SMS broadcasting application using Node.js, the Express framework, and the Plivo Communications Platform. We will cover everything from project setup and core implementation to error handling, security, and deployment, enabling you to build a system capable of handling large-scale messaging campaigns.

Project Goals:

  • Create a Node.js Express application that accepts a list of phone numbers and a message text via an API endpoint.
  • Efficiently send the message to all recipients using Plivo's bulk messaging capabilities.
  • Handle potential errors gracefully with logging and basic retry mechanisms.
  • Implement essential security measures like rate limiting.
  • Provide instructions for setup, testing, and deployment.

Technology Stack:

  • Node.js: A JavaScript runtime environment ideal for building scalable, asynchronous network applications.
  • Express.js: A minimal and flexible Node.js web application framework providing robust features for web and mobile applications.
  • Plivo: A cloud communications platform providing SMS (and Voice) APIs. We'll use its Node.js SDK and bulk messaging feature.
  • dotenv: A module to load environment variables from a .env file into process.env.
  • express-rate-limit: Middleware for basic rate limiting to protect the API endpoint.
  • winston: A versatile logging library.

System Architecture:

text
+-------------+      +-------------------------+      +----------------+      +-----------------+
|   Client    |----->| Node.js / Express App   |----->|   Plivo API    |----->| Mobile Carriers |-----> Recipients
| (e.g. curl, | HTTP | (API Endpoint /send-bulk)| REST | (Bulk Messaging) | SMS  |                 |
| Postman, UI)| POST |                         | API  |                |      |                 |
+-------------+      +-------------------------+      +----------------+      +-----------------+
                         |        ^
                         |        | Log Events / Errors
                         v        |
                      +-----------+
                      |  Logger   |
                      | (Winston) |
                      +-----------+

(Note: The rendering of this text-based diagram might vary depending on your Markdown viewer and font settings.)

Prerequisites:

  • Node.js and npm (or yarn): Installed on your system. Download from nodejs.org.
  • Plivo Account: Sign up for a free trial account at plivo.com.
  • Plivo Auth ID and Auth Token: Found on your Plivo Console dashboard.
  • A Plivo Phone Number or Sender ID: Capable of sending SMS messages. Purchase a number or configure a Sender ID in the Plivo Console. Note: Sending to US/Canada requires a Plivo phone number. Trial accounts can only send to verified sandbox numbers.
  • Basic understanding of JavaScript, Node.js, and REST APIs.

By the end of this guide, you will have a functional Express API endpoint capable of accepting bulk SMS requests and processing them efficiently using Plivo.

1. Setting up the Project

Let's initialize our Node.js project and install the necessary dependencies.

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

    bash
    mkdir plivo-bulk-sms
    cd plivo-bulk-sms
  2. Initialize Node.js Project: This creates a package.json file to manage project dependencies and scripts.

    bash
    npm init -y

    (The -y flag accepts default settings).

  3. Install Dependencies: We need Express for the web server, the Plivo SDK, dotenv for environment variables, winston for logging, and express-rate-limit for security.

    bash
    npm install express plivo-node dotenv winston express-rate-limit
  4. Create Project Structure: Organize the project files for better maintainability.

    text
    plivo-bulk-sms/
    ├── node_modules/
    ├── src/
    │   ├── controllers/
    │   │   └── messageController.js
    │   ├── routes/
    │   │   └── api.js
    │   ├── services/
    │   │   └── plivoService.js
    │   ├── utils/
    │   │   └── logger.js
    │   └── app.js
    ├── .env
    ├── .gitignore
    └── package.json
    • src/: Contains the main application code.
    • src/controllers/: Handles incoming requests and interacts with services.
    • src/routes/: Defines the API endpoints.
    • src/services/: Contains business logic, like interacting with the Plivo API.
    • src/utils/: Utility functions, like logging setup.
    • src/app.js: The main Express application setup.
    • .env: Stores sensitive configuration (API keys, etc.). Never commit this file.
    • .gitignore: Specifies files/directories Git should ignore.
  5. Create .gitignore: Create a file named .gitignore in the project root and add the following lines to prevent committing sensitive information and unnecessary files:

    text
    # Dependencies
    node_modules/
    
    # Environment variables
    .env
    
    # Logs
    logs/
    *.log
    
    # OS generated files
    .DS_Store
    Thumbs.db
  6. Create .env File: Create a file named .env in the project root. This is where we'll store our Plivo credentials and other configurations.

    dotenv
    # Plivo Credentials - **REPLACE THESE WITH YOUR ACTUAL CREDENTIALS**
    PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
    PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
    PLIVO_SENDER_ID=YOUR_PLIVO_NUMBER_OR_SENDER_ID # e.g., +14155551234
    
    # Application Settings
    PORT=3000
    LOG_LEVEL=info
    
    # Plivo Bulk Send Limit (as per Plivo docs)
    PLIVO_BULK_LIMIT=1000
    
    # Optional: Webhook URL for Delivery Reports
    # WEBHOOK_URL=https://your-callback-url.com/api/v1/delivery-reports

    Important:

    • You must replace YOUR_PLIVO_AUTH_ID and YOUR_PLIVO_AUTH_TOKEN with the actual credentials found on your Plivo Console Dashboard.
    • You must replace YOUR_PLIVO_NUMBER_OR_SENDER_ID with the Plivo phone number (in E.164 format, e.g., +14155551234) or approved Alphanumeric Sender ID you will use to send messages. Manage these under the "Phone Numbers" section in the Plivo Console.
    • PORT: The port your Express application will listen on.
    • LOG_LEVEL: Controls the verbosity of logs (e.g., error, warn, info, debug).
    • PLIVO_BULK_LIMIT: Plivo's documented limit for destination numbers per single API call. Currently 1000.
    • WEBHOOK_URL: (Optional) If you plan to implement delivery report tracking, uncomment and set this to your publicly accessible callback URL.

2. Implementing Core Functionality (Plivo Service)

We'll encapsulate the Plivo interaction logic within a dedicated service file. This service will handle initializing the Plivo client and the core bulk sending logic, including batching recipients.

  1. Set up Logger Utility (src/utils/logger.js): A robust logging setup is crucial for monitoring and debugging.

    javascript
    // src/utils/logger.js
    const winston = require('winston');
    require('dotenv').config(); // Ensure .env variables are loaded early
    
    const logFormat = winston.format.printf(({ level, message, timestamp, stack }) => {
      return `${timestamp} ${level}: ${stack || message}`;
    });
    
    const logger = winston.createLogger({
      level: process.env.LOG_LEVEL || 'info',
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
        winston.format.errors({ stack: true }), // Log stack traces
        logFormat
      ),
      transports: [
        new winston.transports.Console(),
        // Optionally add file transport
        // new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
        // new winston.transports.File({ filename: 'logs/combined.log' }),
      ],
      exceptionHandlers: [
        // Log unhandled exceptions to console and/or file
        new winston.transports.Console(),
        // new winston.transports.File({ filename: 'logs/exceptions.log' })
      ],
      rejectionHandlers: [
        // Log unhandled promise rejections
        new winston.transports.Console(),
        // new winston.transports.File({ filename: 'logs/rejections.log' })
      ]
    });
    
    module.exports = logger;
    • This sets up Winston with console logging, timestamps, colorization, and stack trace logging for errors.
    • It reads the LOG_LEVEL from the .env file.
    • It includes handlers for uncaught exceptions and unhandled promise rejections.
  2. Create Plivo Service (src/services/plivoService.js): This file handles communication with the Plivo API.

    javascript
    // src/services/plivoService.js
    const plivo = require('plivo-node');
    const logger = require('../utils/logger');
    require('dotenv').config(); // Load .env variables
    
    // --- Configuration ---
    const authId = process.env.PLIVO_AUTH_ID;
    const authToken = process.env.PLIVO_AUTH_TOKEN;
    const senderId = process.env.PLIVO_SENDER_ID;
    const bulkLimit = parseInt(process.env.PLIVO_BULK_LIMIT || '1000', 10);
    const webhookUrl = process.env.WEBHOOK_URL; // Optional webhook URL
    
    if (!authId || !authToken || !senderId) {
      logger.error('FATAL: Plivo Auth ID, Auth Token, or Sender ID missing in .env file. Application cannot function.');
      // Exit the application if core configuration is missing.
      // This prevents the server from running in a state where it cannot send SMS.
      process.exit(1);
    }
    
    // --- Plivo Client Initialization ---
    // Check again (though process should exit above if missing)
    const client = (authId && authToken) ? new plivo.Client(authId, authToken) : null;
    
    if (!client) {
        // This case should ideally not be reached due to the check above, but serves as a safeguard.
        logger.error('FATAL: Failed to initialize Plivo client. Check credentials and Plivo SDK installation.');
        process.exit(1);
    } else {
        logger.info('Plivo client initialized successfully.');
    }
    
    
    // --- Core Bulk Send Function ---
    /**
     * Sends an SMS message to multiple recipients using Plivo's bulk messaging.
     * Handles batching recipients according to Plivo's limits.
     *
     * @param {string[]} recipients - An array of destination phone numbers in E.164 format.
     * @param {string} messageText - The text content of the SMS message.
     * @returns {Promise<object>} - A promise that resolves with aggregated results or rejects on fatal error.
     */
    const sendBulkSms = async (recipients, messageText) => {
      if (!client) {
          // This check handles cases where the service might be called unexpectedly after a failed init
          logger.error('Plivo client is not available. Cannot send messages.');
          throw new Error('Plivo client is not initialized. Check application logs.');
      }
      if (!recipients || recipients.length === 0) {
        throw new Error('Recipient list cannot be empty.');
      }
      if (!messageText) {
        throw new Error('Message text cannot be empty.');
      }
    
      logger.info(`Starting bulk SMS send job for ${recipients.length} recipients.`);
    
      const results = {
        success: [],
        failed: [],
        batchesAttempted: 0,
        batchesSucceeded: 0,
        batchesFailed: 0,
      };
    
      // Split recipients into batches based on Plivo's limit
      for (let i = 0; i < recipients.length; i += bulkLimit) {
        const batch = recipients.slice(i, i + bulkLimit);
        const destinationNumbers = batch.join('<'); // Plivo uses '<' as delimiter
        results.batchesAttempted++;
    
        logger.debug(`Sending batch ${results.batchesAttempted}: ${batch.length} numbers.`);
    
        try {
          // Construct parameters for Plivo API call
          const params = {
            src: senderId,
            dst: destinationNumbers,
            text: messageText,
            // Optional: Add powerpack_uuid, log: true, trackable: true, etc.
          };
    
          // Add webhook URL if configured in .env
          if (webhookUrl) {
            params.url = webhookUrl;
            logger.debug(`Using delivery report webhook URL: ${webhookUrl}`);
          }
    
          const response = await client.messages.create(params);
    
          logger.info(`Plivo API response for batch ${results.batchesAttempted}: ${JSON.stringify(response)}`);
    
          // Assuming success if API call doesn't throw. Plivo's response indicates acceptance, not delivery.
          // The response.messageUuid often contains UUIDs for *each* message in the batch.
          results.success.push(...batch.map(num => ({ number: num, apiResponse: response })));
          results.batchesSucceeded++;
    
        } catch (error) {
          logger.error(`Failed to send batch ${results.batchesAttempted} to Plivo: ${error.message}`, { stack: error.stack, batch });
          results.failed.push(...batch.map(num => ({ number: num, error: error.message })));
          results.batchesFailed++;
          // Decide if one batch failure should stop the whole job or continue
          // For this example, we continue with other batches.
        }
      }
    
      logger.info(`Bulk SMS job finished. ${results.batchesSucceeded} batches succeeded, ${results.batchesFailed} batches failed.`);
      return results;
    };
    
    module.exports = {
      sendBulkSms,
    };
    • Initialization: It loads credentials from .env, performs a critical check for their existence, and exits the application (process.exit(1)) if they are missing, preventing the server from starting without the ability to send SMS. It then initializes the Plivo client.
    • Batching: The sendBulkSms function takes an array of recipients and the message text. It iterates through the recipients, slicing them into batches based on PLIVO_BULK_LIMIT.
    • Delimiter: Inside the loop, batch.join('<') creates the Plivo-specific <-delimited string for the dst parameter.
    • API Call: client.messages.create is called for each batch. We use async/await. The optional url parameter for delivery reports is added if WEBHOOK_URL is set in the environment.
    • Error Handling: A try...catch block wraps the API call. If an error occurs for a batch, it's logged, and the failed numbers are recorded. The loop continues to the next batch. A check ensures the Plivo client is available before attempting to send.
    • Response: The function returns a summary object detailing successful and failed numbers/batches.

3. Building the API Layer (Controller and Routes)

Now, let's create the Express controller and route to expose our bulk SMS functionality via an HTTP API.

  1. Create Message Controller (src/controllers/messageController.js): This handles the logic for the /send-bulk endpoint.

    javascript
    // src/controllers/messageController.js
    const plivoService = require('../services/plivoService');
    const logger = require('../utils/logger');
    
    // Basic E.164 format regex: Starts with '+', followed by 1 to 15 digits.
    // Note: This checks format only, not whether the number is actually valid or reachable.
    const E164_REGEX = /^\+\d{1,15}$/;
    
    const handleBulkSend = async (req, res) => {
      const { recipients, message } = req.body;
    
      // --- Input Validation ---
      if (!Array.isArray(recipients) || recipients.length === 0) {
        logger.warn('Received invalid or empty recipients list.');
        return res.status(400).json({ success: false, error: 'Invalid input: "recipients" must be a non-empty array of phone numbers.' });
      }
      if (typeof message !== 'string' || message.trim() === '') {
        logger.warn('Received invalid or empty message text.');
        return res.status(400).json({ success: false, error: 'Invalid input: "message" must be a non-empty string.' });
      }
    
      // Validate phone number format (basic E.164 check)
      // For stricter validation (e.g., checking country code validity or using libraries like libphonenumber-js),
      // enhance this part as needed.
      const invalidNumbers = recipients.filter(num => typeof num !== 'string' || !E164_REGEX.test(num));
      if (invalidNumbers.length > 0) {
          logger.warn(`Received invalid phone number formats: ${invalidNumbers.join(', ')}`);
          return res.status(400).json({
              success: false,
              error: `Invalid E.164 format for phone numbers: ${invalidNumbers.join(', ')}. Ensure numbers start with '+' followed by country code and number (1-15 digits total).`
          });
      }
    
      try {
        logger.info(`Received bulk send request for ${recipients.length} recipients.`);
        const result = await plivoService.sendBulkSms(recipients, message);
    
        // Determine overall status based on batch results
        const overallSuccess = result.batchesFailed === 0;
        const statusCode = overallSuccess ? 200 : (result.batchesSucceeded > 0 ? 207 : 500); // 200 OK, 207 Multi-Status, 500 Internal Server Error
    
        logger.info(`Bulk send request processed. Status code: ${statusCode}`);
        res.status(statusCode).json({
          success: overallSuccess,
          message: `Processed ${result.batchesAttempted} batches. ${result.batchesSucceeded} succeeded, ${result.batchesFailed} failed.`,
          details: result, // Contains breakdown of success/failed numbers
        });
    
      } catch (error) {
        logger.error(`Error processing bulk send request: ${error.message}`, { stack: error.stack });
        // Check for specific error types if needed
        if (error.message.includes('Plivo client is not initialized')) {
             // This indicates a server configuration issue caught during the request
             return res.status(503).json({ success: false, error: 'SMS service unavailable due to configuration issue. Please contact administrator.' });
        }
        // Generic error for other unexpected issues during processing
        res.status(500).json({ success: false, error: 'Internal server error while sending messages.' });
      }
    };
    
    module.exports = {
      handleBulkSend,
    };
    • Validation: It performs essential validation: checks if recipients is a non-empty array and message is a non-empty string. It includes a basic regex check (E164_REGEX) for E.164 format, noting that this only checks the pattern, not the number's real-world validity.
    • Service Call: It calls plivoService.sendBulkSms with the validated data.
    • Response Handling: It constructs a JSON response based on the result from the service. It uses status code 200 if all batches succeed, 207 (Multi-Status) if some succeed and some fail, and 500 if all fail or a critical error occurs (like the Plivo client being unavailable, resulting in a 503).
  2. Create API Routes (src/routes/api.js): Defines the endpoint(s) and links them to controller functions or handlers.

    javascript
    // src/routes/api.js
    const express = require('express');
    const messageController = require('../controllers/messageController');
    const rateLimit = require('express-rate-limit');
    const logger = require('../utils/logger');
    
    const router = express.Router();
    
    // --- Rate Limiting ---
    // Apply rate limiting to protect the bulk send endpoint from abuse
    const bulkSendLimiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per windowMs
      message: { success: false, error: 'Too many requests, please try again after 15 minutes.' },
      handler: (req, res, next, options) => {
          logger.warn(`Rate limit exceeded for IP: ${req.ip} on ${req.originalUrl}`);
          res.status(options.statusCode).send(options.message);
      },
      standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
      legacyHeaders: false, // Disable the `X-RateLimit-*` headers
    });
    
    // --- API Endpoint Definition ---
    
    // POST /api/v1/send-bulk: Endpoint to send bulk messages
    router.post('/send-bulk', bulkSendLimiter, messageController.handleBulkSend);
    
    // POST /api/v1/delivery-reports: Endpoint to receive delivery status webhooks from Plivo
    router.post('/delivery-reports', (req, res) => {
        logger.info('Received Plivo Delivery Report:', { body: req.body, headers: req.headers });
    
        // **IMPORTANT**: Add your logic here to process the delivery report.
        // 1. Parse req.body (Plivo sends form-urlencoded data by default,
        //    ensure express.urlencoded middleware is used in app.js).
        // 2. Extract relevant fields (MessageUUID, Status, ErrorCode, etc.).
        // 3. Update your database or tracking system with the delivery status.
        // 4. Handle potential errors during processing.
    
        // Always respond with 200 OK quickly to acknowledge receipt to Plivo.
        // Processing should ideally happen asynchronously if it's complex.
        res.status(200).send('Delivery report received.');
    });
    
    
    module.exports = router;
    • Rate Limiter: Uses express-rate-limit for the /send-bulk endpoint. Customize windowMs and max as needed. Includes logging when the limit is hit.
    • Route Definitions:
      • Defines POST /api/v1/send-bulk, applies the rate limiter, and maps it to messageController.handleBulkSend.
      • Defines POST /api/v1/delivery-reports as a basic webhook receiver. This endpoint is not rate-limited by default, as traffic comes from Plivo IPs.

4. Setting up the Express Application (src/app.js)

This file ties everything together: initializes Express, loads middleware, mounts the API routes, and starts the server.

javascript
// src/app.js
const express = require('express');
const dotenv = require('dotenv');
const apiRoutes = require('./routes/api');
const logger = require('./utils/logger');

// Load environment variables from .env file FIRST
dotenv.config();

// Now, check for critical env vars before proceeding further
// (plivoService also checks, but this ensures Express doesn't start without them)
if (!process.env.PLIVO_AUTH_ID || !process.env.PLIVO_AUTH_TOKEN || !process.env.PLIVO_SENDER_ID) {
    logger.error('FATAL: Missing required Plivo credentials in environment variables. Shutting down.');
    process.exit(1);
}

const app = express();
const port = process.env.PORT || 3000;

// --- Middleware ---
// Enable parsing of JSON request bodies (for /send-bulk)
app.use(express.json({ limit: '10mb' })); // Increase limit if expecting very large recipient lists
// Enable parsing of URL-encoded request bodies (needed for Plivo webhooks)
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Basic request logging middleware
app.use((req, res, next) => {
  logger.info(`Incoming Request: ${req.method} ${req.originalUrl} - IP: ${req.ip}`);
  const start = process.hrtime();

  res.on('finish', () => {
    const diff = process.hrtime(start);
    const responseTime = (diff[0] * 1e3 + diff[1] * 1e-6).toFixed(3); // milliseconds
    logger.info(`Request Handled: ${res.statusCode} ${res.statusMessage} - ${req.method} ${req.originalUrl} - Response Time: ${responseTime}ms`);
  });
  next();
});

// --- API Routes ---
app.use('/api/v1', apiRoutes); // Mount API routes under /api/v1 prefix

// --- Health Check Endpoint ---
app.get('/health', (req, res) => {
  // Basic health check confirms the server is running
  res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});

// --- Not Found Handler ---
// Catch 404s for routes not defined
app.use((req, res, next) => {
    res.status(404).json({ success: false, error: 'Not Found' });
});

// --- Global Error Handler ---
// Catch-all for errors passed via next(err) or thrown in async routes
// Must have 4 arguments (err, req, res, next) to be recognized as an error handler
app.use((err, req, res, next) => {
  logger.error('Unhandled error:', {
      message: err.message,
      stack: err.stack,
      url: req.originalUrl,
      method: req.method,
      ip: req.ip
  });

  // Avoid leaking stack traces in production
  const statusCode = err.status || err.statusCode || 500; // Use error's status if available
  res.status(statusCode).json({
      success: false,
      error: (process.env.NODE_ENV === 'production' && statusCode === 500)
          ? 'Internal Server Error'
          : err.message || 'An unexpected error occurred',
      // Optionally include stack trace in development only
      ...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
  });
});

// --- Start Server ---
// The check for credentials is now done at the top
app.listen(port, () => {
  logger.info(`Server listening on port ${port}`);
  logger.info(`Bulk SMS API available at http://localhost:${port}/api/v1/send-bulk`);
  if (process.env.WEBHOOK_URL) {
      logger.info(`Delivery Report endpoint configured at http://localhost:${port}/api/v1/delivery-reports`);
  } else {
      logger.warn('Delivery Report endpoint URL (WEBHOOK_URL) not set in .env - Delivery status tracking is disabled.');
  }
});

module.exports = app; // Export for potential testing
  • Credential Check: Critical check for Plivo credentials moved to the top after dotenv.config() to ensure the application exits immediately if they are missing.
  • Middleware: Includes express.json() and crucially express.urlencoded({ extended: true }) which is needed to parse the default application/x-www-form-urlencoded format used by Plivo webhooks. Request logging middleware is included.
  • Routes: Mounts the API routes from src/routes/api.js.
  • Health Check: Basic /health endpoint.
  • Error Handling: Includes a 404 handler for undefined routes and a global error handler that logs errors and sends appropriate responses (hiding stack traces in production).
  • Server Start: Starts the Express server, logging the API and potentially the webhook endpoint URL.

5. Running and Testing the Application

  1. Start the Server: From your project's root directory (plivo-bulk-sms/), run:

    bash
    node src/app.js

    You should see log output indicating the server is running and the Plivo client initialized. If credentials were missing, it should have logged an error and exited.

  2. Test with curl (or Postman): Open a new terminal window and use curl to send a POST request to your API endpoint.

    • Replace placeholders:
      • Use valid phone numbers in E.164 format (e.g., +15551234567, +447700900123).
      • If using a Plivo Trial account, these numbers must be verified sandbox numbers added in your Plivo Console under Phone Numbers > Sandbox Numbers.
      • Adjust the message text as desired.
    bash
    curl -X POST http://localhost:3000/api/v1/send-bulk \
    -H "Content-Type: application/json" \
    -d '{
      "recipients": [
        "+15551234567",
        "+14155550001",
        "+447700900123"
      ],
      "message": "Hello from your Plivo Bulk Sender! (Test)"
    }'
  3. Expected Response (Success Example): If successful, you should receive a response similar to this (details might vary):

    json
    {
      "success": true,
      "message": "Processed 1 batches. 1 succeeded, 0 failed.",
      "details": {
        "success": [
          {
            "number": "+15551234567",
            "apiResponse": {
              "message": "message(s) queued",
              "messageUuid": [
                "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1",
                "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx2",
                "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx3"
              ],
              "apiId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx0"
            }
          },
          {
            "number": "+14155550001",
            "apiResponse": { /* ... same API response ... */ }
          },
          {
            "number": "+447700900123",
            "apiResponse": { /* ... same API response ... */ }
          }
        ],
        "failed": [],
        "batchesAttempted": 1,
        "batchesSucceeded": 1,
        "batchesFailed": 0
      }
    }
  4. Expected Response (Partial Failure Example): If one batch failed (e.g., due to a temporary Plivo issue):

    json
    {
      "success": false,
      "message": "Processed 2 batches. 1 succeeded, 1 failed.",
      "details": {
        "success": [
          { "number": "+15551234567", "apiResponse": { /* ... */ } }
        ],
        "failed": [
           { "number": "+447700900999", "error": "Plivo API error message here" }
        ],
        "batchesAttempted": 2,
        "batchesSucceeded": 1,
        "batchesFailed": 1
      }
    }

    (Status code would be 207 Multi-Status)

  5. Check Logs: Monitor the terminal where the server is running (node src/app.js). You should see logs for incoming requests, batch processing attempts, Plivo API responses, and any errors. Also check the Plivo Console under Logs > SMS for message records.

6. Implementing Retries (Basic Example)

Network glitches or temporary service issues can cause API calls to fail. Implementing a simple retry mechanism can improve reliability. We can add this to the plivoService.js.

Note: Sophisticated retry logic often involves exponential backoff, jitter, and considers more specific error types (e.g., 5xx vs 4xx). This is a basic illustration.

javascript
// src/services/plivoService.js (Modified section)
// ... (imports, config, client init remain the same) ...

const MAX_RETRIES = 2; // Number of retries per batch (total attempts = 1 + MAX_RETRIES)
const RETRY_DELAY_MS = 1000; // Delay between retries (simple fixed delay)

// Helper function for delayed execution
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

// --- Core Bulk Send Function (with Retries) ---
const sendBulkSms = async (recipients, messageText) => {
  if (!client) { /* ... client check ... */ }
  if (!recipients || recipients.length === 0) { /* ... recipient check ... */ }
  if (!messageText) { /* ... message check ... */ }

  logger.info(`Starting bulk SMS send job for ${recipients.length} recipients with up to ${MAX_RETRIES} retries per batch.`);

  const results = {
    success: [],
    failed: [],
    batchesAttempted: 0,
    batchesSucceeded: 0,
    batchesFailed: 0,
  };

  for (let i = 0; i < recipients.length; i += bulkLimit) {
    const batch = recipients.slice(i, i + bulkLimit);
    const destinationNumbers = batch.join('<');
    results.batchesAttempted++;
    const batchNumber = results.batchesAttempted; // For clearer logging
    logger.debug(`Processing batch ${batchNumber}: ${batch.length} numbers.`);

    let attempts = 0;
    let batchSuccess = false;
    let lastError = null;

    while (attempts <= MAX_RETRIES && !batchSuccess) {
      attempts++;
      if (attempts > 1) {
          logger.warn(`Retrying batch ${batchNumber}, attempt ${attempts} of ${MAX_RETRIES + 1} after ${RETRY_DELAY_MS}ms delay.`);
          await delay(RETRY_DELAY_MS);
      }

      try {
        const params = { src: senderId, dst: destinationNumbers, text: messageText };
        if (webhookUrl) params.url = webhookUrl;

        const response = await client.messages.create(params);

        logger.info(`Plivo API response for batch ${batchNumber} (attempt ${attempts}): ${JSON.stringify(response)}`);
        results.success.push(...batch.map(num => ({ number: num, apiResponse: response, attempt: attempts })));
        results.batchesSucceeded++;
        batchSuccess = true;

      } catch (error) {
        lastError = error;
        logger.error(`Attempt ${attempts} failed for batch ${batchNumber}: ${error.message}`, { stack: error.stack });
        if (attempts > MAX_RETRIES) {
            logger.error(`Batch ${batchNumber} failed after ${MAX_RETRIES + 1} attempts. Recording failures.`);
            results.failed.push(...batch.map(num => ({ number: num, error: lastError.message })));
            results.batchesFailed++;
        }
      }
    }
  }

  logger.info(`Bulk SMS job finished. ${results.batchesSucceeded} batches succeeded, ${results.batchesFailed} batches failed.`);
  return results;
};

module.exports = {
  sendBulkSms,
};

Frequently Asked Questions

How to send bulk SMS with Node.js and Plivo?

Use the Plivo Node.js SDK and the provided code example to create an Express.js application with a /send-bulk endpoint. This endpoint accepts an array of recipient phone numbers and a message text, then uses the Plivo API to send messages efficiently in batches.

What is Plivo's bulk messaging limit?

Plivo's bulk messaging limit is currently 1000 destination numbers per API call. The provided code example handles batching recipient lists exceeding this limit to ensure proper delivery.

Why use a separate service for Plivo interaction?

Separating Plivo logic into a dedicated service (plivoService.js) improves code organization and maintainability. This promotes modularity and makes testing easier.

When should I implement rate limiting for SMS sending?

Rate limiting is essential to protect your SMS API endpoint from abuse. Implement rate limiting to prevent excessive requests by setting reasonable limits (e.g. 100 requests per 15 minutes).

Can I receive delivery reports for my bulk messages?

Yes, set the WEBHOOK_URL environment variable to your delivery report callback URL. Implement logic in the /delivery-reports endpoint to process statuses from Plivo.

How to setup a Node.js project for bulk SMS?

Initialize a Node.js project, install required dependencies (express, plivo-node, dotenv, winston, express-rate-limit), and create a project structure for controllers, routes, services, and utilities.

What is the role of express-rate-limit?

express-rate-limit middleware protects your API from excessive requests by limiting the number of requests from an IP address within a specific timeframe.

How does the bulk SMS sender handle errors?

The example application handles errors via comprehensive logging using Winston and HTTP status codes, and implements basic retry mechanisms to increase resilience against temporary failures.

What is the function of the message controller?

The message controller receives incoming requests, validates input, and interacts with the Plivo service to send bulk SMS messages. It also handles errors and constructs appropriate responses.

How to test the bulk SMS application?

Test the application with curl or Postman, send POST requests to the /send-bulk endpoint. Ensure to use valid E.164 phone numbers, especially with a Plivo trial account.

How are environment variables managed?

Environment variables, such as API keys, are stored in the .env file and loaded into the application using the dotenv module. This file should never be committed to version control.

How to improve reliability of the bulk SMS sender?

Implement retry logic in the Plivo service. This handles temporary failures like network glitches or Plivo issues. Consider exponential backoff and jitter for optimal retry strategies.

What technology stack does the bulk SMS broadcaster use?

The application uses Node.js with Express.js for the web server, the Plivo Node.js SDK for sending SMS, dotenv for managing environment variables, Winston for logging, and express-rate-limit for security.

Why use Winston for logging in the application?

Winston provides a robust and versatile logging library for the application, allowing for structured logging, different log levels (error, warn, info, debug), and handlers for uncaught exceptions and promise rejections.