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.
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).
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.
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.
.env
# 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.jsrequire('dotenv').config(); // Load .env file variablesconst 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 validationif (!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.jsconst plivo = require('plivo-node');
const plivoConfig = require('../../config/plivoConfig');
// Initialize Plivo client onceconst 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.
*/asyncfunctionsendBulkSms(recipients, message) {
if (!recipients || recipients.length === 0) {
thrownewError('Recipient list cannot be empty.');
}
if (!message) {
thrownewError('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 numbersconst 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 delayif (i + batchSize < recipients.length && plivoConfig.batchDelayMs > 0) {
console.log(`Waiting ${plivoConfig.batchDelayMs} ms before next batch…`);
awaitnewPromise(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.thrownewError(`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.
A simple API Key authentication middleware. In production, consider more robust methods like JWT or OAuth2.
JavaScript
// src/middleware/auth.jsrequire('dotenv').config();
constAPI_KEY = process.env.API_KEY;
if (!API_KEY) {
console.warn('Warning: API_KEY environment variable not set. Authentication is disabled.');
}
functionauthenticateApiKey(req, res, next) {
// Skip authentication if API_KEY is not set (useful for local dev/testing)if (!API_KEY) {
returnnext();
}
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.jsconst express = require('express');
const { body, validationResult } = require('express-validator');
const plivoService = require('../services/plivoService');
const authenticateApiKey = require('../middleware/auth'); // Import auth middlewareconst 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 emptybody('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 formatbody('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 stringbody('message')
.isString()
.notEmpty()
.withMessage('Message must be a non-empty string.')
.trim(), // Remove leading/trailing whitespace
],
async (req, res) => {
// Check for validation errorsconst 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 SMSconst 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.
Find Auth ID and Auth Token: On the main dashboard after logging in, you'll see your AUTH ID and AUTH TOKEN.
Copy Credentials: Carefully copy these values.
Update .env: Paste the AUTH ID into PLIVO_AUTH_ID and the AUTH TOKEN into PLIVO_AUTH_TOKEN in your .env file.
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.
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.
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.
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.jsconst winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info', // Log level from env or default to infoformat: 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 logstransports: [
// Default console transportnew 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
asyncfunctionsendSingleBatchWithRetry(payload, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
// logger.info(`Attempt ${attempt + 1} to send batch…`, { dst: payload.dst }); // Use loggerreturnawait client.messages.create(payload); // Attempt API call
} catch (error) {
attempt++;
// Check if the error is potentially retryableconst statusCode = error.statusCode || (error.response && error.response.status); // Plivo SDK might have statusCode or response.statusconst isRetryable = (statusCode >= 500 || statusCode === 429/* Too Many Requests */);
// logger.warn(`Batch send attempt ${attempt} failed.`, { error: error.message, statusCode, isRetryable }); // Use loggerif (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 loggerawaitnewPromise(resolve =>setTimeout(resolve, delay));
} else {
// logger.error(`Final attempt ${attempt} failed or error not retryable.`, { error: error.message, stack: error.stack }); // Use loggerthrow 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 tableCREATE TABLE broadcasts (
id UUID PRIMARY KEYDEFAULT gen_random_uuid(),
message_text TEXT NOT NULL,
status VARCHAR(20) NOT NULLDEFAULT'pending', -- e.g., 'pending', 'processing', 'completed', 'failed'
created_at TIMESTAMPTZ DEFAULT NOW(),
processed_at TIMESTAMPTZ
);
-- broadcast_recipients tableCREATE TABLE broadcast_recipients (
id BIGSERIAL PRIMARY KEY,
broadcast_id UUID NOT NULLREFERENCES broadcasts(id) ONDELETE CASCADE,
recipient_number VARCHAR(20) NOT NULL,
plivo_message_uuid VARCHAR(64) NULLUNIQUE, -- Stores the ID Plivo returns for tracking
status VARCHAR(20) NOT NULLDEFAULT'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 routesconst limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 100, // Limit each IP to 100 requests per windowMsmessage: 'Too many requests from this IP, please try again after 15 minutes',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headerslegacyHeaders: 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.
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
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
Technologies You'll Use:
.envfile intoprocess.env.winstonorpino) and a rate-limiting middleware (express-rate-limit).System Architecture:
Prerequisites:
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.
mkdir plivo-bulk-sms-broadcaster cd plivo-bulk-sms-broadcaster2. Initialize Node.js Project:
Create a
package.jsonfile to manage dependencies and project metadata.3. Install Dependencies:
Install Express for the web server, the Plivo SDK, and
dotenvfor managing environment variables. Addexpress-validatorfor request validation andexpress-rate-limitfor basic security.4. Project Structure:
Create a basic structure for better organization.
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 .gitignoreYour structure should look like this:
5. Configure
.gitignore:Add
node_modulesand.envto your.gitignorefile to avoid committing dependencies and sensitive credentials.6. Setup Environment Variables (
.env):Create a
.envfile in the project root. This file stores sensitive information like API keys and configuration settings. Never commit this file to version control.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.// 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;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 ofsrc/server.jsorsrc/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.
// 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, };async/awaitfor clear asynchronous code.dstparameter correctly usingjoin('<')per Plivo's bulk messaging specification (Plivo Bulk Messaging).forloop andslice, respecting thePLIVO_BULK_MESSAGE_LIMITof 1,000 unique destination numbers.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.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.
// 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;3.2. Broadcast Route (
src/routes/broadcast.js):This file defines the
/broadcastendpoint, validates the request, and calls theplivoService.// 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;express.Routerfor modular routing.authenticateApiKeymiddleware.express-validatorfor robust request validation:recipientsis a non-empty array.recipients.*) in the array against an E.164 regex pattern.messageis a non-empty string.plivoService.sendBulkSmsfunction.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:
AUTH IDandAUTH TOKEN..env: Paste theAUTH IDintoPLIVO_AUTH_IDand theAUTH TOKENintoPLIVO_AUTH_TOKENin your.envfile..env: Copy the chosen Plivo phone number (in E.164 format, e.g.,+14155551212) or the approved Sender ID string intoPLIVO_SENDER_IDin your.envfile.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
.envfile 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:
express-validatorin the route, returning 400 Bad Request with details.auth.jsmiddleware, returning 401 Unauthorized or 403 Forbidden.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).try…catchblocks in the service and route, logged, and result in a 500 Internal Server Error.Logging:
We've used basic
console.logandconsole.error. For production, use a structured logging library:npm install winston # Or pino, etc.Example: Basic Winston Setup (e.g., in a new file
config/logger.js)// 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 });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.
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 correctlyisRetryablecheck (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.statusCodemight 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:
This typically requires a database.
Conceptual Schema (e.g., PostgreSQL):
-- 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:
createBroadcast,addRecipients,updateMessageStatus).broadcast_recipientsstatus. Plivo sends delivery status callbacks with attributes includingMessageUUID,Status(queued,sent,failed,delivered,undelivered), andErrorCodefor 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). Ifmessagecontent could ever come from user input, sanitize it to prevent XSS or injection attacks (though less likely for SMS). Libraries likeDOMPurify(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).
// 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);windowMsandmaxbased 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
helmetmiddleware for setting various security-related HTTP headers.// 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
.envfiles.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:
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_numberparameter of the response. Valid destinations receive uniquemessage_uuidvalues 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:
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 thedstparameter. 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 uniquemessage_uuidfor 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_numberresponse parameter and provides uniquemessage_uuidvalues 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-limitto prevent abuse (Plivo enforces 300 API requests per 5 seconds), HTTPS for all traffic, helmet.js middleware for security headers, input validation usingexpress-validatorwith 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
urlandmethodparameters. Create an endpoint to receive POST/GET requests from Plivo containing delivery status updates with attributes includingMessageUUID,Status(queued,sent,delivered,undelivered,failed,readfor 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.