code examples

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

Plivo Bulk SMS Node.js Tutorial: Send Mass SMS Messages with Express.js API

A guide to building a bulk SMS broadcasting system using Node.js, Express, and Plivo, covering setup, implementation, error handling, security, and deployment.

Send Bulk SMS with Plivo, Node.js & Express: Production Broadcasting Guide

Learn how to send bulk SMS messages with Plivo and Node.js in this comprehensive tutorial. Build a production-ready mass messaging API using Express.js that efficiently broadcasts SMS to thousands of recipients with proper batching, rate limiting, and error handling.

This guide covers everything from Plivo API integration and bulk message batching to security best practices and delivery status tracking. By the end, you'll have a robust REST API endpoint that accepts recipient lists and message content, then efficiently broadcasts messages using Plivo's bulk SMS capabilities.

Plivo Bulk SMS Broadcasting: Project Overview

What You're Building:

You'll build a Node.js application using Express that exposes an API endpoint. This endpoint receives requests containing a list of recipient phone numbers and a message body. Your application then leverages Plivo's API to send this message efficiently to all specified recipients in bulk.

Problems This Solves:

  • Inefficient Individual Sends: Sending SMS messages one by one via separate API calls is slow and resource-intensive for large lists.
  • Scalability: Provides a foundation for sending messages to large audiences without overwhelming your system or hitting API rate limits improperly.
  • Centralized Control: Offers a single API endpoint to manage broadcast operations.

Technologies You'll Use:

  • Node.js: A JavaScript runtime environment ideal for building scalable, non-blocking, I/O-heavy applications like API servers.
  • Express.js: A minimal and flexible Node.js web application framework providing routing, middleware, and other essential features.
  • Plivo Node.js SDK: Simplifies interaction with the Plivo REST API for sending SMS. Learn more in our Plivo Node.js basic SMS tutorial.
  • dotenv: A zero-dependency module that loads environment variables from a .env file into process.env.
  • (Optional but Recommended) A logging library (like winston or pino) and a rate-limiting middleware (express-rate-limit).

System Architecture:

text
+-------------+       +------------------------+       +----------------+       +-----------------+
|   Client    | ----> | Node.js/Express API    | ----> |   Plivo API    | ----> | Recipient Phones|
| (e.g. curl, |       | (POST /broadcast)      |       | (Bulk Message) |       | (SMS Delivery)  |
|   WebApp)   |       | - Validate Request     |       +----------------+       +-----------------+
+-------------+       | - Authenticate         |
                      | - Format Numbers (Batch)|
                      | - Call Plivo SDK       |
                      +------------------------+
                             |
                             | (Logs/Errors)
                             V
                      +------------------------+
                      | Logging/Monitoring Svc |
                      +------------------------+

Prerequisites:

  • Node.js and npm (or yarn): Install Node.js on your development machine. (Download Node.js)
  • Plivo Account: Create a Plivo account with Auth ID, Auth Token, and at least one Plivo phone number (or Sender ID where applicable). (Sign up for Plivo)
    • Trial Account Limitation: If you're using a trial account, you must add and verify the phone numbers you intend to send messages to in your Plivo console under "Phone Numbers" > "Sandbox Numbers." You cannot send to unverified numbers.
  • Basic understanding of: JavaScript, Node.js, Express, REST APIs, and Promises/async-await. For SMS basics, see our guide on E.164 phone number format.

Step 1: Node.js Project Setup for Plivo Bulk SMS

Initialize your 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-broadcaster
cd plivo-bulk-sms-broadcaster

2. Initialize Node.js Project:

Create a package.json file to manage dependencies and project metadata.

bash
npm init -y

3. Install Dependencies:

Install Express for the web server, the Plivo SDK, and dotenv for managing environment variables. Add express-validator for request validation and express-rate-limit for basic security.

bash
npm install express plivo-node dotenv express-validator express-rate-limit

4. Project Structure:

Create a basic structure for better organization.

bash
mkdir src
mkdir src/routes
mkdir src/services
mkdir src/middleware
mkdir config
touch src/app.js
touch src/server.js
touch src/routes/broadcast.js
touch src/services/plivoService.js
touch src/middleware/auth.js
touch config/plivoConfig.js
touch .env
touch .gitignore

Your structure should look like this:

text
plivo-bulk-sms-broadcaster/
├── config/
│   └── plivoConfig.js
├── node_modules/
├── src/
│   ├── app.js             # Express app configuration
│   ├── server.js          # Server startup logic
│   ├── middleware/
│   │   └── auth.js        # API Key authentication
│   ├── routes/
│   │   └── broadcast.js   # Broadcast API route
│   └── services/
│       └── plivoService.js # Plivo interaction logic
├── .env                   # Environment variables (API keys, etc.) – DO NOT COMMIT
├── .gitignore             # Files/folders to ignore in Git
├── package.json
└── package-lock.json

5. Configure .gitignore:

Add node_modules and .env to your .gitignore file to avoid committing dependencies and sensitive credentials.

text
# Dependencies
node_modules/

# Environment variables
.env

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

6. Setup Environment Variables (.env):

Create a .env file in the project root. This file stores sensitive information like API keys and configuration settings. Never commit this file to version control.

dotenv
# 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., +14155551212

# API Configuration
PORT=3000
API_KEY=YOUR_SECRET_API_KEY_FOR_THIS_SERVICE # Generate a strong, random key

# Plivo Settings (Optional overrides)
PLIVO_BULK_MESSAGE_LIMIT=1000 # Max recipients per Plivo API call
PLIVO_BATCH_DELAY_MS=250 # Delay between sending batches (to respect rate limits)
  • PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN: Find these on your Plivo Console dashboard.
  • PLIVO_SENDER_ID: The Plivo phone number (in E.164 format, e.g., +14155551212) or approved Alphanumeric Sender ID you'll use to send messages. Restrictions apply – US/Canada require registered phone numbers. Learn about US SMS registration requirements.
  • API_KEY: A secret key you define. Clients calling your API need to provide this key for authentication. Generate a strong, random string for this value.
  • PLIVO_BULK_MESSAGE_LIMIT: Plivo's documented limit per API call is 1,000 unique destination numbers (Plivo Bulk Messaging API).
  • PLIVO_BATCH_DELAY_MS: A delay between batch submissions. Plivo uses intelligent queueing that dequeues messages based on account-level rate limits configured for your account. While you can send API requests at any rate, messages are throttled for delivery based on source number type and destination country. A 250 ms delay provides safe spacing between batch submissions (Plivo Rate Limiting).

7. Plivo Configuration (config/plivoConfig.js):

Load Plivo credentials and settings securely using dotenv.

javascript
// config/plivoConfig.js
require('dotenv').config(); // Load .env file variables

const plivoConfig = {
  authId: process.env.PLIVO_AUTH_ID,
  authToken: process.env.PLIVO_AUTH_TOKEN,
  senderId: process.env.PLIVO_SENDER_ID,
  bulkMessageLimit: parseInt(process.env.PLIVO_BULK_MESSAGE_LIMIT || '1000', 10),
  batchDelayMs: parseInt(process.env.PLIVO_BATCH_DELAY_MS || '250', 10),
};

// Basic validation
if (!plivoConfig.authId || !plivoConfig.authToken || !plivoConfig.senderId) {
  console.error(
    'Error: Plivo Auth ID, Auth Token, or Sender ID not found in environment variables.'
  );
  console.error('Please check your .env file.');
  process.exit(1); // Exit if essential config is missing
}

module.exports = plivoConfig;
  • Why: This centralizes Plivo configuration, reads securely from environment variables, and performs basic validation on startup.
  • Note: While placing require('dotenv').config() here works, a common practice is to load it only once at the very beginning of your application's entry point (e.g., top of src/server.js or src/app.js) to ensure environment variables are available globally before any other modules are loaded. This approach avoids potential issues with load order and keeps configuration loading centralized.

Step 2: Implementing Plivo Bulk SMS Service with Message Batching

This service encapsulates the logic for interacting with the Plivo API, including formatting numbers for bulk sending and handling batching.

javascript
// src/services/plivoService.js
const plivo = require('plivo-node');
const plivoConfig = require('../../config/plivoConfig');

// Initialize Plivo client once
const client = new plivo.Client(plivoConfig.authId, plivoConfig.authToken);

/**
 * Sends a message to multiple recipients using Plivo's bulk messaging.
 * Handles batching if the recipient list exceeds Plivo's limit.
 *
 * @param {string[]} recipients – Array of recipient phone numbers in E.164 format.
 * @param {string} message – The text message body.
 * @returns {Promise<object[]>} – A promise that resolves with an array of Plivo API responses for each batch sent.
 */
async function sendBulkSms(recipients, message) {
  if (!recipients || recipients.length === 0) {
    throw new Error('Recipient list cannot be empty.');
  }
  if (!message) {
    throw new Error('Message body cannot be empty.');
  }

  const results = [];
  const batchSize = plivoConfig.bulkMessageLimit;

  console.log(`Sending message to ${recipients.length} recipients in batches of ${batchSize}`);

  for (let i = 0; i < recipients.length; i += batchSize) {
    const batch = recipients.slice(i, i + batchSize);
    const batchNumber = Math.floor(i / batchSize) + 1;

    // Plivo requires '<' delimiter for bulk destination numbers
    const destinationNumbers = batch.join('<');

    const payload = {
      src: plivoConfig.senderId,
      dst: destinationNumbers,
      text: message,
      // Optional: Add URL for delivery reports
      // url: 'YOUR_DELIVERY_REPORT_WEBHOOK_URL',
      // method: 'POST'
    };

    try {
      console.log(`Sending batch ${batchNumber} to ${batch.length} recipients…`);
      const response = await client.messages.create(payload);
      console.log(`Batch ${batchNumber} sent successfully. Plivo Response:`, response);
      results.push(response);

      // If there are more batches to send, add a small delay
      if (i + batchSize < recipients.length && plivoConfig.batchDelayMs > 0) {
        console.log(`Waiting ${plivoConfig.batchDelayMs} ms before next batch…`);
        await new Promise(resolve => setTimeout(resolve, plivoConfig.batchDelayMs));
      }

    } catch (error) {
      console.error(`Error sending batch ${batchNumber}:`, error);
      // Handle potential Plivo API errors within the loop.
      // Production Consideration: The current implementation throws an error
      // and stops processing immediately if any batch fails. For more robust
      // bulk sending, consider implementing a partial failure strategy:
      // 1. Collect errors for failed batches in a separate array.
      // 2. Continue processing subsequent batches.
      // 3. After the loop, if any errors occurred, either throw an aggregated
      //    error or return a result object indicating partial success and
      //    listing the specific failures.
      // This prevents one bad batch from halting the entire broadcast.
      throw new Error(`Failed to send SMS batch ${batchNumber}: ${error.message || error}`); // Current: Stop on first error
    }
  }

  console.log('All batches processed.');
  return results; // Array of Plivo responses for each successful batch
}

module.exports = {
  sendBulkSms,
};
  • Why:
    • Encapsulates Plivo logic, making the API route cleaner.
    • Uses async/await for clear asynchronous code.
    • Formats the dst parameter correctly using join('<') per Plivo's bulk messaging specification (Plivo Bulk Messaging).
    • Implements batching using a for loop and slice, respecting the PLIVO_BULK_MESSAGE_LIMIT of 1,000 unique destination numbers.
    • Includes a configurable delay (PLIVO_BATCH_DELAY_MS) between batches. Plivo's smart queueing system processes messages in order and handles rate limiting automatically, but spacing batch submissions helps prevent queue congestion.
    • Provides basic logging for visibility.
    • Includes basic input validation.
    • Handles potential Plivo API errors within the loop.

Step 3: Building Express.js Bulk SMS API Endpoint

Now, create the Express route that will receive broadcast requests.

3.1. Authentication Middleware (src/middleware/auth.js):

A simple API Key authentication middleware. In production, consider more robust methods like JWT or OAuth2.

javascript
// src/middleware/auth.js
require('dotenv').config();

const API_KEY = process.env.API_KEY;

if (!API_KEY) {
  console.warn('Warning: API_KEY environment variable not set. Authentication is disabled.');
}

function authenticateApiKey(req, res, next) {
  // Skip authentication if API_KEY is not set (useful for local dev/testing)
  if (!API_KEY) {
    return next();
  }

  const providedApiKey = req.headers['x-api-key']; // Common practice uses 'x-api-key', but standard alternatives like 'Authorization: ApiKey YOUR_KEY' or 'Authorization: Bearer YOUR_KEY' are often preferred.

  if (!providedApiKey) {
    return res.status(401).json({ message: 'Unauthorized: API key missing.' });
  }

  if (providedApiKey !== API_KEY) {
    return res.status(403).json({ message: 'Forbidden: Invalid API key.' });
  }

  next(); // API key is valid, proceed to the next middleware/route handler
}

module.exports = authenticateApiKey;
  • Why: Protects your API endpoint from unauthorized access using a simple shared secret passed in the header.

3.2. Broadcast Route (src/routes/broadcast.js):

This file defines the /broadcast endpoint, validates the request, and calls the plivoService.

javascript
// src/routes/broadcast.js
const express = require('express');
const { body, validationResult } = require('express-validator');
const plivoService = require('../services/plivoService');
const authenticateApiKey = require('../middleware/auth'); // Import auth middleware

const router = express.Router();

// E.164 regex pattern:
// ^\+ : Starts with a literal '+' sign.
// [1-9]: Followed by a digit from 1 to 9 (avoids +0).
// \d{1,14}: Followed by 1 to 14 digits (total length 3 to 16 chars including +).
// $: End of string.
// This is a common representation, adjust if specific country code lengths need stricter validation.
const e164Pattern = /^\+[1-9]\d{1,14}$/;

// Apply API Key authentication to this route
router.use(authenticateApiKey);

router.post(
  '/',
  [
    // 1. Validate recipients: must be an array, not empty
    body('recipients')
      .isArray({ min: 1 })
      .withMessage('Recipients must be a non-empty array.'),
    // 2. Validate each recipient in the array: must be a string matching E.164 format
    body('recipients.*') // The '*' applies validation to each element in the array
      .isString()
      .matches(e164Pattern)
      .withMessage('Each recipient must be a string in E.164 format (e.g., +14155551212).'),
    // 3. Validate message: must be a non-empty string
    body('message')
      .isString()
      .notEmpty()
      .withMessage('Message must be a non-empty string.')
      .trim(), // Remove leading/trailing whitespace
  ],
  async (req, res) => {
    // Check for validation errors
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    const { recipients, message } = req.body;

    try {
      // Call the service to send the SMS
      const results = await plivoService.sendBulkSms(recipients, message);

      // Respond with success status and Plivo's response details
      // 202 Accepted is suitable as the processing might take time,
      // even though we await here. It indicates the request is valid
      // and processing has begun (or completed).
      res.status(202).json({
        message: `Broadcast request accepted for ${recipients.length} recipients.`,
        plivoResponses: results, // Contains response from each batch sent
      });
    } catch (error) {
      console.error('Error in /broadcast route:', error);
      // Determine appropriate status code based on error type
      // If it's a Plivo API error passed up, it might indicate bad input
      // or Plivo service issues. If it's an internal error, use 500.
      res.status(500).json({
        message: 'Failed to process broadcast request.',
        error: error.message || 'Internal Server Error',
      });
    }
  }
);

module.exports = router;
  • Why:
    • Uses express.Router for modular routing.
    • Integrates authenticateApiKey middleware.
    • Leverages express-validator for robust request validation:
      • Checks if recipients is a non-empty array.
      • Validates each element (recipients.*) in the array against an E.164 regex pattern.
      • Checks if message is a non-empty string.
    • Returns detailed validation errors (400 Bad Request).
    • Calls the plivoService.sendBulkSms function.
    • Provides informative success (202 Accepted) and error (500 Internal Server Error or potentially 4xx for specific Plivo errors) responses in JSON format.

Step 4: Plivo API Configuration and Credentials Setup

We already integrated the Plivo SDK in the service layer. This section focuses on obtaining and configuring the necessary credentials.

Steps to get Plivo Credentials:

  1. Log in to your Plivo Console: https://console.plivo.com/
  2. Find Auth ID and Auth Token: On the main dashboard after logging in, you'll see your AUTH ID and AUTH TOKEN.
  3. Copy Credentials: Carefully copy these values.
  4. Update .env: Paste the AUTH ID into PLIVO_AUTH_ID and the AUTH TOKEN into PLIVO_AUTH_TOKEN in your .env file.
  5. Obtain/Configure Sender ID:
    • Navigate to "Phone Numbers" > "Your Numbers" in the Plivo console.
    • If you don't have a number, go to "Buy Numbers" and purchase one suitable for SMS in your target regions (e.g., a US long code for sending to the US/Canada).
    • If using an Alphanumeric Sender ID (where supported), ensure it's registered and approved via the "Messaging" > "Sender IDs" section.
  6. Update .env: Copy the chosen Plivo phone number (in E.164 format, e.g., +14155551212) or the approved Sender ID string into PLIVO_SENDER_ID in your .env file.
  7. Sandbox Numbers (Trial Accounts Only):
    • If using a trial account, go to "Phone Numbers" > "Sandbox Numbers."
    • Add and verify the phone numbers you intend to send messages to during testing. You cannot send to unverified numbers with a trial account.

Environment Variable Explanation:

  • PLIVO_AUTH_ID (String): Your Plivo account identifier. Purpose: Authentication. Format: Alphanumeric string. Obtain: Plivo Console Dashboard.
  • PLIVO_AUTH_TOKEN (String): Your Plivo account secret token. Purpose: Authentication. Format: Alphanumeric string. Obtain: Plivo Console Dashboard.
  • PLIVO_SENDER_ID (String): The "from" number or ID for outgoing SMS. Purpose: Identify the sender to the recipient and comply with regulations. Format: E.164 phone number (+1…) or approved Alphanumeric string. Obtain: Plivo Console Phone Numbers/Sender IDs section.
  • API_KEY (String): A secret key for securing your own API endpoint. Purpose: Basic authentication for your service. Format: Strong, random string. Obtain: You generate this yourself.
  • PORT (Number): Port number for the Express server. Purpose: Network configuration. Format: Integer (e.g., 3000). Obtain: Choose an available port.
  • PLIVO_BULK_MESSAGE_LIMIT (Number): Max recipients per Plivo API call (1,000 unique destination numbers). Purpose: Control batch size. Format: Integer. Obtain: Plivo Bulk Messaging Documentation.
  • PLIVO_BATCH_DELAY_MS (Number): Milliseconds to wait between sending batches. Purpose: Rate limiting and queue management. Format: Integer. Obtain: Based on your Plivo account limits and testing.

Security: Ensure your .env file is never committed to Git and has restrictive file permissions on your server. Use secrets management tools (like AWS Secrets Manager, HashiCorp Vault, Doppler) in production environments.

Step 5: Error Handling and Retry Logic for Bulk SMS

Robust error handling and logging are crucial for production systems.

Error Handling Strategy:

  • Validation Errors: Handled by express-validator in the route, returning 400 Bad Request with details.
  • Authentication Errors: Handled by auth.js middleware, returning 401 Unauthorized or 403 Forbidden.
  • Plivo API Errors: Caught within plivoService.js. The Plivo SDK throws errors containing status codes and messages from the Plivo API. These are logged and currently re-thrown, causing the route handler to return a 500 Internal Server Error. Improvement: Inspect the Plivo error status code and return more specific 4xx errors if appropriate (e.g., 400 for invalid number format if not caught by validation, 402 Payment Required for insufficient funds).
  • Network/Unexpected Errors: Caught by the try…catch blocks in the service and route, logged, and result in a 500 Internal Server Error.

Logging:

We've used basic console.log and console.error. For production, use a structured logging library:

bash
npm install winston # Or pino, etc.

Example: Basic Winston Setup (e.g., in a new file config/logger.js)

javascript
// config/logger.js
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info', // Log level from env or default to info
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }), // Log stack traces
    winston.format.json() // Log in JSON format
  ),
  defaultMeta: { service: 'sms-broadcaster' }, // Optional: Add service name to logs
  transports: [
    // Default console transport
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(), // Add colors for readability in console
        winston.format.simple()
      )
    })
    // In production, add transports for files or log management services:
    // new winston.transports.File({ filename: 'error.log', level: 'error' }),
    // new winston.transports.File({ filename: 'combined.log' }),
  ],
});

// Example: Add file transport only if not in development
// if (process.env.NODE_ENV !== 'development') {
//   logger.add(new winston.transports.File({ filename: 'combined.log' }));
// }

module.exports = logger;

// --- Usage Example (replace console.log/error in other files): ---
// const logger = require('../../config/logger'); // Adjust path as needed

// logger.info('Broadcast request received.', { recipientCount: recipients.length });
// logger.warn('API Key not set, authentication disabled.');
// logger.error('Failed to send SMS batch', { batchNumber: 1, error: error.message, stack: error.stack });
  • Why: Structured logs (JSON) are easier to parse, filter, and analyze in log management systems. Libraries like Winston offer features like log levels, multiple transports (console, file, external services), and customizable formatting.

Retry Mechanisms:

The current implementation stops processing if a batch fails. For transient errors (network timeouts, temporary Plivo issues, rate limiting), implement retries with exponential backoff.

javascript
async function sendSingleBatchWithRetry(payload, maxRetries = 3) {
  let attempt = 0;
  while (attempt < maxRetries) {
    try {
      // logger.info(`Attempt ${attempt + 1} to send batch…`, { dst: payload.dst }); // Use logger
      return await client.messages.create(payload); // Attempt API call
    } catch (error) {
      attempt++;
      // Check if the error is potentially retryable
      const statusCode = error.statusCode || (error.response && error.response.status); // Plivo SDK might have statusCode or response.status
      const isRetryable = (statusCode >= 500 || statusCode === 429 /* Too Many Requests */);
      // logger.warn(`Batch send attempt ${attempt} failed.`, { error: error.message, statusCode, isRetryable }); // Use logger

      if (isRetryable && attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 100; // Exponential backoff (100 ms, 200 ms, 400 ms…)
        // logger.warn(`Retrying in ${delay} ms…`); // Use logger
        await new Promise(resolve => setTimeout(resolve, delay));
      } else {
        // logger.error(`Final attempt ${attempt} failed or error not retryable.`, { error: error.message, stack: error.stack }); // Use logger
        throw error; // Re-throw the error if max retries reached or not retryable
      }
    }
  }
}

// Inside the sendBulkSms loop:
// Replace: const response = await client.messages.create(payload);
// With:    const response = await sendSingleBatchWithRetry(payload); // Assuming payload is defined correctly
  • Why: Makes the service more resilient to temporary network or service issues without manual intervention.
  • Important Caveats:
    • Identifying Retryable Errors: The isRetryable check (statusCode >= 500 || statusCode === 429) is a basic example. You must carefully examine the specific error codes and structures returned by the Plivo Node.js SDK for different failure scenarios (e.g., network issues, specific Plivo API errors) to determine accurately which errors are truly transient and safe to retry. Blindly retrying non-transient errors (like invalid credentials or insufficient funds) is wasteful and can hide underlying problems.
    • Error Structure: Relying solely on statusCode might not be sufficient. The Plivo SDK error object might contain more specific codes or messages within its body that are better indicators for retry logic. Thorough testing against Plivo's error responses is essential.

Step 6: Database Schema for SMS Campaign Tracking

While this guide focuses on stateless broadcasting, a production system often needs to:

  • Manage recipient lists.
  • Track broadcast campaigns.
  • Store message status (sent, delivered, failed) using Plivo's Delivery Reports.
  • Handle unsubscribes.

This typically requires a database.

Conceptual Schema (e.g., PostgreSQL):

sql
-- broadcasts table
CREATE TABLE broadcasts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    message_text TEXT NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending', -- e.g., 'pending', 'processing', 'completed', 'failed'
    created_at TIMESTAMPTZ DEFAULT NOW(),
    processed_at TIMESTAMPTZ
);

-- broadcast_recipients table
CREATE TABLE broadcast_recipients (
    id BIGSERIAL PRIMARY KEY,
    broadcast_id UUID NOT NULL REFERENCES broadcasts(id) ON DELETE CASCADE,
    recipient_number VARCHAR(20) NOT NULL,
    plivo_message_uuid VARCHAR(64) NULL UNIQUE, -- Stores the ID Plivo returns for tracking
    status VARCHAR(20) NOT NULL DEFAULT 'queued', -- e.g., 'queued', 'sent', 'delivered', 'undelivered', 'failed'
    status_updated_at TIMESTAMPTZ,
    error_code VARCHAR(10) NULL, -- Plivo error code if failed
    INDEX idx_broadcast_recipients_broadcast_id (broadcast_id),
    INDEX idx_broadcast_recipients_status (status)
);

Implementation:

  • Use an ORM (like Sequelize, Prisma, TypeORM) or a query builder (like Knex.js).
  • Implement data access functions (e.g., createBroadcast, addRecipients, updateMessageStatus).
  • Set up database migrations to manage schema changes.
  • Create a webhook endpoint to receive Delivery Reports from Plivo and update the broadcast_recipients status. Plivo sends delivery status callbacks with attributes including MessageUUID, Status (queued, sent, failed, delivered, undelivered), and ErrorCode for failed messages (Plivo Message Status Callbacks). For related implementation, see our guide on Plivo delivery status callbacks.

This detailed database implementation is beyond the scope of this specific guide but is a critical next step for a full-featured production system.

Step 7: Security Best Practices for Bulk SMS APIs

Security is paramount. We've added basic auth, but consider these:

  • Input Validation & Sanitization: Already implemented using express-validator. Ensure validation is strict (e.g., tight regex for numbers). If message content could ever come from user input, sanitize it to prevent XSS or injection attacks (though less likely for SMS). Libraries like DOMPurify (if rendering HTML somewhere) or basic escaping might be relevant depending on context.

  • Rate Limiting (API Endpoint): Protect your own API from abuse. Note that Plivo enforces a default limit of 300 API requests per 5 seconds across all APIs (Plivo Account Limits).

    javascript
    // src/app.js (Add this)
    const rateLimit = require('express-rate-limit');
    
    // Apply rate limiting to all requests or specific routes
    const limiter = 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 the rate limiting middleware to all requests
    // Place it early, but potentially after static file serving if any
    app.use(limiter);
    • Why: Prevents brute-force attacks or single users overwhelming your service. Adjust windowMs and max based on expected usage.
  • HTTPS: Always use HTTPS in production. Deploy behind a reverse proxy (like Nginx or Caddy) or use hosting platforms (Heroku, Cloud Run) that handle TLS termination.

  • Helmet.js: Use the helmet middleware for setting various security-related HTTP headers.

    bash
    npm install helmet
    javascript
    // src/app.js (Add this near the top, after body-parser if used, before routes)
    const helmet = require('helmet');
    app.use(helmet()); // Sets various security headers like Content-Security-Policy, X-Frame-Options etc.
  • Secrets Management: Use proper secrets management tools for API keys and database credentials in production (AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, Doppler). Do not store secrets directly in code or commit .env files.

Step 8: SMS Encoding, Character Limits, and Compliance

Real-world messaging has nuances:

  • E.164 Format: Strictly enforce the E.164 format (+ followed by country code and number, no spaces or dashes) for recipient numbers. Our validation helps, but ensure upstream data sources are clean. The API automatically verifies each destination number format and removes duplicates (Plivo Bulk Messaging).

  • Character Limits & Encoding: SMS message encoding and segmentation depend on character types:

    • GSM-7 encoding (standard characters): Maximum 1,600 characters. Single-part messages support 160 characters; multi-part messages use 153 characters per segment (7 bytes reserved for User Data Header).
    • UCS-2 encoding (Unicode/emojis): Maximum 737 characters. Single-part messages support 70 characters; multi-part messages use 67 characters per segment.

    Plivo automatically handles segmentation and billing is per segment (Plivo Encoding and Concatenation).

  • Sender ID Restrictions: US/Canada require using Plivo phone numbers (long codes or toll-free) as Sender IDs. Alphanumeric Sender IDs are supported in many other countries but often require pre-registration and may have restrictions (e.g., cannot receive replies). Check Plivo's documentation for the target countries.

  • Invalid Numbers: The Plivo API validates destination numbers and returns invalid ones in the invalid_number parameter of the response. Valid destinations receive unique message_uuid values for tracking (Plivo Bulk Messaging).

  • Opt-Outs/Compliance: Implement mechanisms to handle STOP/HELP keywords and maintain opt-out lists as required by regulations (e.g., TCPA in the US). Plivo offers features to help manage compliance. This often involves processing inbound messages or using Plivo's opt-out management features. Learn more about SMS compliance requirements.

Step 9: Performance Optimization for High-Volume SMS Broadcasting

For high-volume broadcasting:

  • Batching: Already implemented – this is the most significant optimization for bulk sending via Plivo, reducing API call overhead.
  • Asynchronous Processing: For very large lists (> thousands), making the API endpoint wait for all batches to complete can lead to client timeouts.
    • Strategy: Accept the request, validate it, potentially store it in a database (see Step 6), and return a job ID immediately. Process batches asynchronously using a queue system (Bull, BeeQueue, or cloud-based queues). Provide a separate endpoint to check job status. Note that messages may not be delivered instantly – Plivo's smart queue dequeues messages based on rate limits and processes them in order (Plivo Rate Limiting).

Frequently Asked Questions (FAQ)

What is the maximum number of recipients I can send to in a single Plivo bulk SMS request?

Plivo supports up to 1,000 unique destination numbers per API call when using bulk messaging with the < delimiter (Plivo Bulk Messaging API). If you need to send to more recipients, implement batching logic that splits your recipient list into groups of 1,000 or fewer and processes each batch sequentially with appropriate delays between batches.

How does Plivo bulk messaging differ from sending individual SMS messages?

Bulk messaging uses a single API call to send the same message to multiple recipients by joining phone numbers with the < delimiter in the dst parameter. This approach significantly reduces API overhead, network latency, and processing time compared to making separate API calls for each recipient. The API validates each destination number, removes duplicates, and returns a unique message_uuid for each valid recipient. However, each recipient still receives an individual message and is billed separately.

What rate limits should I consider when implementing bulk SMS broadcasting?

You can send API requests to Plivo at any rate, but messages are queued and dequeued based on account-level rate limits configured for your account. Plivo uses smart queueing that throttles delivery based on source number type and destination country. Multipart messages are dequeued as single entities with auto-adjusted rates (Plivo Rate Limiting). Additionally, Plivo enforces a default limit of 300 API requests per 5 seconds across all APIs (Plivo Account Limits). Implement delays between batches (250–500 ms recommended) and monitor for HTTP 429 rate limiting errors.

How do I handle partial failures when broadcasting to large recipient lists?

Implement a partial failure strategy by collecting errors for failed batches in a separate array, continuing to process subsequent batches, and returning a detailed result object indicating which batches succeeded and which failed. This prevents one bad batch from halting the entire broadcast. Plivo returns invalid destination numbers in the invalid_number response parameter and provides unique message_uuid values for valid recipients. Store broadcast results in a database to track individual message statuses using Plivo's delivery reports (Plivo Bulk Messaging).

What's the difference between GSM-7 and UCS-2 encoding for SMS messages?

GSM-7 is the standard SMS encoding that supports 160 characters per single message and 153 characters per segment for multi-part messages. It includes basic Latin characters, numbers, and common symbols from the GSM 03.38 character set. UCS-2 encoding is automatically used when your message contains emojis, certain accented characters, or non-Latin scripts, but reduces the limit to 70 characters per single message and 67 characters per segment for multi-part messages. Both encodings support maximum message lengths of 1,600 characters (GSM-7) and 737 characters (UCS-2). Each segment is billed separately by Plivo (Plivo Encoding and Concatenation).

Can I use alphanumeric sender IDs for bulk SMS broadcasting in the United States?

No. The United States and Canada require using registered phone numbers (10-digit long codes or toll-free numbers) as sender IDs for SMS. Alphanumeric sender IDs are supported in many other countries but require pre-registration with Plivo and have limitations – they cannot receive replies and may have country-specific restrictions. Always check Plivo's sender ID documentation for your target regions.

How do I implement TCPA compliance and handle opt-out requests?

Implement a database table to track opt-out statuses, filter opted-out numbers from your recipient lists before sending, and set up an inbound message webhook to process STOP, UNSTOP, and HELP keywords automatically. Plivo provides opt-out management features through their console. Always obtain explicit consent before sending marketing messages and honor opt-out requests immediately to comply with TCPA regulations.

What security measures should I implement for a production bulk SMS API?

Essential security measures include: API key authentication (or JWT/OAuth2 for production), rate limiting using express-rate-limit to prevent abuse (Plivo enforces 300 API requests per 5 seconds), HTTPS for all traffic, helmet.js middleware for security headers, input validation using express-validator with strict E.164 format checking, secrets management using dedicated tools (AWS Secrets Manager, HashiCorp Vault), and proper error handling that doesn't expose sensitive information.

How can I track delivery status for bulk SMS messages?

Configure a delivery report webhook URL in your Plivo message payload using the url and method parameters. Create an endpoint to receive POST/GET requests from Plivo containing delivery status updates with attributes including MessageUUID, Status (queued, sent, delivered, undelivered, failed, read for WhatsApp), ErrorCode, timestamps, and billing information. Store these updates in a database with the Plivo message UUID as the identifier (Plivo Message Status Callbacks).

What's the best way to test bulk SMS broadcasting without incurring costs?

Use Plivo's trial account, which allows you to send messages to verified sandbox numbers at no cost. Add recipient phone numbers to your sandbox in the Plivo console under "Phone Numbers" > "Sandbox Numbers" and verify them via SMS. Test your batching logic, error handling, and rate limiting strategies using these verified numbers before upgrading to a paid account for production use. Trial accounts cannot send to unverified numbers.

Frequently Asked Questions

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

Use the Plivo Node.js SDK with Express.js to create an API endpoint that accepts recipient numbers and a message, then sends the message in bulk via Plivo's API, handling batching and rate limits as needed. This setup is much more efficient than sending individual SMS messages and is designed for scalability and centralized control.

What is Plivo bulk message limit?

Plivo's bulk message limit is 1000 recipients per API call. The provided code handles batching automatically, dividing larger recipient lists into chunks of 1000 or less to comply with this limitation, with a configurable delay between batches to manage rate limits.

Why use bulk SMS instead of individual sends?

Sending individual SMS messages via separate API calls becomes inefficient and resource-intensive for large lists. Bulk sending offers better performance, scalability, and easier management through a centralized API endpoint.

When should I use a logging library in my project?

A logging library like Winston or Pino is highly recommended for production systems, though not strictly required for basic functionality. It provides structured logs, log levels, and different output options (console, file, external services), which are crucial for debugging, monitoring, and analysis.

Can I use alphanumeric sender IDs with Plivo?

Yes, you can use alphanumeric sender IDs with Plivo, but with restrictions. They are generally supported outside the US and Canada, but may require pre-registration and may have limitations like not being able to receive replies. In the US and Canada, you must use a Plivo phone number (long code or toll-free).

How to handle Plivo API errors in Node.js?

The Plivo Node.js SDK throws errors that contain status codes and messages from the Plivo API. Implement try-catch blocks in your service layer to handle these errors gracefully. You can also inspect error status codes to provide more specific error responses to your API clients.

What is E.164 format for phone numbers?

The E.164 format is an international standard for phone numbers. It ensures consistent formatting by requiring a '+' followed by the country code and the number, with no spaces or other characters. Enforcing this format is essential for reliable SMS delivery with Plivo.

How to implement API key authentication for my SMS service?

A simple API key authentication can be added using middleware that checks for an API key in the request headers (e.g., 'x-api-key'). For more robust authentication, consider using JWT (JSON Web Tokens) or OAuth2.

How to set up environment variables for Plivo?

Create a .env file in your project root to store Plivo credentials (Auth ID, Auth Token, Sender ID) and other sensitive information. Use the dotenv package in your Node.js code to load these variables securely. Never commit your .env file to version control.

What's the purpose of express-validator?

Express-validator is a middleware for validating and sanitizing user input in Express.js applications. It helps ensure data integrity and security by enforcing rules on incoming requests, such as checking for required fields, data types, and formats like email addresses or phone numbers.

How to manage SMS character limits with Plivo?

Standard SMS messages have a 160-character limit (GSM-7 encoding). Non-GSM characters reduce this to 70. Plivo automatically segments longer messages, but this may result in multiple messages being billed. Consider informing users about character limits or truncating messages.

Why use rate limiting middleware in Express?

Rate limiting middleware protects your API from abuse by limiting the number of requests from a single IP address within a specific time window. This helps prevent brute-force attacks and ensures fair usage of your service.

How to handle SMS opt-outs and comply with regulations?

Implement mechanisms to handle STOP/HELP keywords and maintain opt-out lists using Plivo's features or a database. This is essential for compliance with regulations like TCPA in the US. Plivo provides features to help manage compliance.

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

A recommended database schema includes tables for broadcasts (message, status, timestamps) and broadcast_recipients (recipient number, Plivo message UUID, status, error codes). Consider using an ORM or query builder for database interaction.