Sending SMS messages individually is straightforward, but scaling to hundreds or thousands of recipients requires a more robust approach. Broadcasting messages efficiently minimizes API calls, reduces latency, and simplifies tracking.
This guide details how to build a reliable bulk SMS broadcasting application using Node.js, the Express framework, and the Plivo Communications Platform. We will cover everything from project setup and core implementation to error handling, security, and deployment, enabling you to build a system capable of handling large-scale messaging campaigns.
Project Goals:
- Create a Node.js Express application that accepts a list of phone numbers and a message text via an API endpoint.
- Efficiently send the message to all recipients using Plivo's bulk messaging capabilities.
- Handle potential errors gracefully with logging and basic retry mechanisms.
- Implement essential security measures like rate limiting.
- Provide instructions for setup, testing, and deployment.
Technology Stack:
- Node.js: A JavaScript runtime environment ideal for building scalable, asynchronous network applications.
- Express.js: A minimal and flexible Node.js web application framework providing robust features for web and mobile applications.
- Plivo: A cloud communications platform providing SMS (and Voice) APIs. We'll use its Node.js SDK and bulk messaging feature.
- dotenv: A module to load environment variables from a
.env
file intoprocess.env
. - express-rate-limit: Middleware for basic rate limiting to protect the API endpoint.
- winston: A versatile logging library.
System Architecture:
+-------------+ +-------------------------+ +----------------+ +-----------------+
| Client |----->| Node.js / Express App |----->| Plivo API |----->| Mobile Carriers |-----> Recipients
| (e.g. curl, | HTTP | (API Endpoint /send-bulk)| REST | (Bulk Messaging) | SMS | |
| Postman, UI)| POST | | API | | | |
+-------------+ +-------------------------+ +----------------+ +-----------------+
| ^
| | Log Events / Errors
v |
+-----------+
| Logger |
| (Winston) |
+-----------+
(Note: The rendering of this text-based diagram might vary depending on your Markdown viewer and font settings.)
Prerequisites:
- Node.js and npm (or yarn): Installed on your system. Download from nodejs.org.
- Plivo Account: Sign up for a free trial account at plivo.com.
- Plivo Auth ID and Auth Token: Found on your Plivo Console dashboard.
- A Plivo Phone Number or Sender ID: Capable of sending SMS messages. Purchase a number or configure a Sender ID in the Plivo Console. Note: Sending to US/Canada requires a Plivo phone number. Trial accounts can only send to verified sandbox numbers.
- Basic understanding of JavaScript, Node.js, and REST APIs.
By the end of this guide, you will have a functional Express API endpoint capable of accepting bulk SMS requests and processing them efficiently using Plivo.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir plivo-bulk-sms cd plivo-bulk-sms
-
Initialize Node.js Project: This creates a
package.json
file to manage project dependencies and scripts.npm init -y
(The
-y
flag accepts default settings). -
Install Dependencies: We need Express for the web server, the Plivo SDK,
dotenv
for environment variables,winston
for logging, andexpress-rate-limit
for security.npm install express plivo-node dotenv winston express-rate-limit
-
Create Project Structure: Organize the project files for better maintainability.
plivo-bulk-sms/ ├── node_modules/ ├── src/ │ ├── controllers/ │ │ └── messageController.js │ ├── routes/ │ │ └── api.js │ ├── services/ │ │ └── plivoService.js │ ├── utils/ │ │ └── logger.js │ └── app.js ├── .env ├── .gitignore └── package.json
src/
: Contains the main application code.src/controllers/
: Handles incoming requests and interacts with services.src/routes/
: Defines the API endpoints.src/services/
: Contains business logic, like interacting with the Plivo API.src/utils/
: Utility functions, like logging setup.src/app.js
: The main Express application setup..env
: Stores sensitive configuration (API keys, etc.). Never commit this file..gitignore
: Specifies files/directories Git should ignore.
-
Create
.gitignore
: Create a file named.gitignore
in the project root and add the following lines to prevent committing sensitive information and unnecessary files:# Dependencies node_modules/ # Environment variables .env # Logs logs/ *.log # OS generated files .DS_Store Thumbs.db
-
Create
.env
File: Create a file named.env
in the project root. This is where we'll store our Plivo credentials and other configurations.# Plivo Credentials - **REPLACE THESE WITH YOUR ACTUAL CREDENTIALS** PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN PLIVO_SENDER_ID=YOUR_PLIVO_NUMBER_OR_SENDER_ID # e.g., +14155551234 # Application Settings PORT=3000 LOG_LEVEL=info # Plivo Bulk Send Limit (as per Plivo docs) PLIVO_BULK_LIMIT=1000 # Optional: Webhook URL for Delivery Reports # WEBHOOK_URL=https://your-callback-url.com/api/v1/delivery-reports
Important:
- You must replace
YOUR_PLIVO_AUTH_ID
andYOUR_PLIVO_AUTH_TOKEN
with the actual credentials found on your Plivo Console Dashboard. - You must replace
YOUR_PLIVO_NUMBER_OR_SENDER_ID
with the Plivo phone number (in E.164 format, e.g.,+14155551234
) or approved Alphanumeric Sender ID you will use to send messages. Manage these under the ""Phone Numbers"" section in the Plivo Console. PORT
: The port your Express application will listen on.LOG_LEVEL
: Controls the verbosity of logs (e.g.,error
,warn
,info
,debug
).PLIVO_BULK_LIMIT
: Plivo's documented limit for destination numbers per single API call. Currently 1000.WEBHOOK_URL
: (Optional) If you plan to implement delivery report tracking (see Section 8), uncomment and set this to your publicly accessible callback URL.
- You must replace
2. Implementing Core Functionality (Plivo Service)
We'll encapsulate the Plivo interaction logic within a dedicated service file. This service will handle initializing the Plivo client and the core bulk sending logic, including batching recipients.
-
Set up Logger Utility (
src/utils/logger.js
): A robust logging setup is crucial for monitoring and debugging.// src/utils/logger.js const winston = require('winston'); require('dotenv').config(); // Ensure .env variables are loaded early const logFormat = winston.format.printf(({ level, message, timestamp, stack }) => { return `${timestamp} ${level}: ${stack || message}`; }); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.colorize(), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), // Log stack traces logFormat ), transports: [ new winston.transports.Console(), // Optionally add file transport // new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), // new winston.transports.File({ filename: 'logs/combined.log' }), ], exceptionHandlers: [ // Log unhandled exceptions to console and/or file new winston.transports.Console(), // new winston.transports.File({ filename: 'logs/exceptions.log' }) ], rejectionHandlers: [ // Log unhandled promise rejections new winston.transports.Console(), // new winston.transports.File({ filename: 'logs/rejections.log' }) ] }); module.exports = logger;
- This sets up Winston with console logging, timestamps, colorization, and stack trace logging for errors.
- It reads the
LOG_LEVEL
from the.env
file. - It includes handlers for uncaught exceptions and unhandled promise rejections.
-
Create Plivo Service (
src/services/plivoService.js
): This file handles communication with the Plivo API.// src/services/plivoService.js const plivo = require('plivo-node'); const logger = require('../utils/logger'); require('dotenv').config(); // Load .env variables // --- Configuration --- const authId = process.env.PLIVO_AUTH_ID; const authToken = process.env.PLIVO_AUTH_TOKEN; const senderId = process.env.PLIVO_SENDER_ID; const bulkLimit = parseInt(process.env.PLIVO_BULK_LIMIT || '1000', 10); const webhookUrl = process.env.WEBHOOK_URL; // Optional webhook URL if (!authId || !authToken || !senderId) { logger.error('FATAL: Plivo Auth ID, Auth Token, or Sender ID missing in .env file. Application cannot function.'); // Exit the application if core configuration is missing. // This prevents the server from running in a state where it cannot send SMS. process.exit(1); } // --- Plivo Client Initialization --- // Check again (though process should exit above if missing) const client = (authId && authToken) ? new plivo.Client(authId, authToken) : null; if (!client) { // This case should ideally not be reached due to the check above, but serves as a safeguard. logger.error('FATAL: Failed to initialize Plivo client. Check credentials and Plivo SDK installation.'); process.exit(1); } else { logger.info('Plivo client initialized successfully.'); } // --- Core Bulk Send Function --- /** * Sends an SMS message to multiple recipients using Plivo's bulk messaging. * Handles batching recipients according to Plivo's limits. * * @param {string[]} recipients - An array of destination phone numbers in E.164 format. * @param {string} messageText - The text content of the SMS message. * @returns {Promise<object>} - A promise that resolves with aggregated results or rejects on fatal error. */ const sendBulkSms = async (recipients, messageText) => { if (!client) { // This check handles cases where the service might be called unexpectedly after a failed init logger.error('Plivo client is not available. Cannot send messages.'); throw new Error('Plivo client is not initialized. Check application logs.'); } if (!recipients || recipients.length === 0) { throw new Error('Recipient list cannot be empty.'); } if (!messageText) { throw new Error('Message text cannot be empty.'); } logger.info(`Starting bulk SMS send job for ${recipients.length} recipients.`); const results = { success: [], failed: [], batchesAttempted: 0, batchesSucceeded: 0, batchesFailed: 0, }; // Split recipients into batches based on Plivo's limit for (let i = 0; i < recipients.length; i += bulkLimit) { const batch = recipients.slice(i_ i + bulkLimit); const destinationNumbers = batch.join('<'); // Plivo uses '<' as delimiter results.batchesAttempted++; logger.debug(`Sending batch ${results.batchesAttempted}: ${batch.length} numbers.`); try { // Construct parameters for Plivo API call const params = { src: senderId_ dst: destinationNumbers_ text: messageText_ // Optional: Add powerpack_uuid_ log: true_ trackable: true_ etc. }; // Add webhook URL if configured in .env if (webhookUrl) { params.url = webhookUrl; logger.debug(`Using delivery report webhook URL: ${webhookUrl}`); } const response = await client.messages.create(params); logger.info(`Plivo API response for batch ${results.batchesAttempted}: ${JSON.stringify(response)}`); // Assuming success if API call doesn't throw. Plivo's response indicates acceptance_ not delivery. // The response.messageUuid often contains UUIDs for *each* message in the batch. results.success.push(...batch.map(num => ({ number: num, apiResponse: response }))); results.batchesSucceeded++; } catch (error) { logger.error(`Failed to send batch ${results.batchesAttempted} to Plivo: ${error.message}`, { stack: error.stack, batch }); results.failed.push(...batch.map(num => ({ number: num, error: error.message }))); results.batchesFailed++; // Decide if one batch failure should stop the whole job or continue // For this example, we continue with other batches. } } logger.info(`Bulk SMS job finished. ${results.batchesSucceeded} batches succeeded, ${results.batchesFailed} batches failed.`); return results; }; module.exports = { sendBulkSms, };
- Initialization: It loads credentials from
.env
, performs a critical check for their existence, and exits the application (process.exit(1)
) if they are missing, preventing the server from starting without the ability to send SMS. It then initializes the Plivo client. - Batching: The
sendBulkSms
function takes an array of recipients and the message text. It iterates through the recipients, slicing them into batches based onPLIVO_BULK_LIMIT
. - Delimiter: Inside the loop,
batch.join('<')
creates the Plivo-specific<
-delimited string for thedst
parameter. - API Call:
client.messages.create
is called for each batch. We useasync/await
. The optionalurl
parameter for delivery reports is added ifWEBHOOK_URL
is set in the environment. - Error Handling: A
try...catch
block wraps the API call. If an error occurs for a batch_ it's logged_ and the failed numbers are recorded. The loop continues to the next batch. A check ensures the Plivo client is available before attempting to send. - Response: The function returns a summary object detailing successful and failed numbers/batches.
- Initialization: It loads credentials from
3. Building the API Layer (Controller and Routes)
Now_ let's create the Express controller and route to expose our bulk SMS functionality via an HTTP API.
-
Create Message Controller (
src/controllers/messageController.js
): This handles the logic for the/send-bulk
endpoint.// src/controllers/messageController.js const plivoService = require('../services/plivoService'); const logger = require('../utils/logger'); // Basic E.164 format regex: Starts with '+'_ followed by 1 to 15 digits. // Note: This checks format only_ not whether the number is actually valid or reachable. const E164_REGEX = /^\+\d{1_15}$/; const handleBulkSend = async (req_ res) => { const { recipients, message } = req.body; // --- Input Validation --- if (!Array.isArray(recipients) || recipients.length === 0) { logger.warn('Received invalid or empty recipients list.'); return res.status(400).json({ success: false, error: 'Invalid input: ""recipients"" must be a non-empty array of phone numbers.' }); } if (typeof message !== 'string' || message.trim() === '') { logger.warn('Received invalid or empty message text.'); return res.status(400).json({ success: false, error: 'Invalid input: ""message"" must be a non-empty string.' }); } // Validate phone number format (basic E.164 check) // For stricter validation (e.g., checking country code validity or using libraries like libphonenumber-js), // enhance this part as needed. const invalidNumbers = recipients.filter(num => typeof num !== 'string' || !E164_REGEX.test(num)); if (invalidNumbers.length > 0) { logger.warn(`Received invalid phone number formats: ${invalidNumbers.join(', ')}`); return res.status(400).json({ success: false, error: `Invalid E.164 format for phone numbers: ${invalidNumbers.join(', ')}. Ensure numbers start with '+' followed by country code and number (1-15 digits total).` }); } try { logger.info(`Received bulk send request for ${recipients.length} recipients.`); const result = await plivoService.sendBulkSms(recipients, message); // Determine overall status based on batch results const overallSuccess = result.batchesFailed === 0; const statusCode = overallSuccess ? 200 : (result.batchesSucceeded > 0 ? 207 : 500); // 200 OK, 207 Multi-Status, 500 Internal Server Error logger.info(`Bulk send request processed. Status code: ${statusCode}`); res.status(statusCode).json({ success: overallSuccess, message: `Processed ${result.batchesAttempted} batches. ${result.batchesSucceeded} succeeded, ${result.batchesFailed} failed.`, details: result, // Contains breakdown of success/failed numbers }); } catch (error) { logger.error(`Error processing bulk send request: ${error.message}`, { stack: error.stack }); // Check for specific error types if needed if (error.message.includes('Plivo client is not initialized')) { // This indicates a server configuration issue caught during the request return res.status(503).json({ success: false, error: 'SMS service unavailable due to configuration issue. Please contact administrator.' }); } // Generic error for other unexpected issues during processing res.status(500).json({ success: false, error: 'Internal server error while sending messages.' }); } }; module.exports = { handleBulkSend, };
- Validation: It performs essential validation: checks if
recipients
is a non-empty array andmessage
is a non-empty string. It includes a basic regex check (E164_REGEX
) for E.164 format, noting that this only checks the pattern, not the number's real-world validity. - Service Call: It calls
plivoService.sendBulkSms
with the validated data. - Response Handling: It constructs a JSON response based on the result from the service. It uses status code
200
if all batches succeed,207
(Multi-Status) if some succeed and some fail, and500
if all fail or a critical error occurs (like the Plivo client being unavailable, resulting in a503
).
- Validation: It performs essential validation: checks if
-
Create API Routes (
src/routes/api.js
): Defines the endpoint(s) and links them to controller functions or handlers.// src/routes/api.js const express = require('express'); const messageController = require('../controllers/messageController'); const rateLimit = require('express-rate-limit'); const logger = require('../utils/logger'); const router = express.Router(); // --- Rate Limiting --- // Apply rate limiting to protect the bulk send endpoint from abuse const bulkSendLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: { success: false, error: 'Too many requests, please try again after 15 minutes.' }, handler: (req, res, next, options) => { logger.warn(`Rate limit exceeded for IP: ${req.ip} on ${req.originalUrl}`); res.status(options.statusCode).send(options.message); }, standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // --- API Endpoint Definition --- // POST /api/v1/send-bulk: Endpoint to send bulk messages router.post('/send-bulk', bulkSendLimiter, messageController.handleBulkSend); // POST /api/v1/delivery-reports: Endpoint to receive delivery status webhooks from Plivo // (See Section 8 for details on enabling this in Plivo) router.post('/delivery-reports', (req, res) => { logger.info('Received Plivo Delivery Report:', { body: req.body, headers: req.headers }); // **IMPORTANT**: Add your logic here to process the delivery report. // 1. Parse req.body (Plivo sends form-urlencoded data by default, // ensure express.urlencoded middleware is used in app.js). // 2. Extract relevant fields (MessageUUID, Status, ErrorCode, etc.). // 3. Update your database or tracking system with the delivery status. // 4. Handle potential errors during processing. // Always respond with 200 OK quickly to acknowledge receipt to Plivo. // Processing should ideally happen asynchronously if it's complex. res.status(200).send('Delivery report received.'); }); module.exports = router;
- Rate Limiter: Uses
express-rate-limit
for the/send-bulk
endpoint. CustomizewindowMs
andmax
as needed. Includes logging when the limit is hit. - Route Definitions:
- Defines
POST /api/v1/send-bulk
, applies the rate limiter, and maps it tomessageController.handleBulkSend
. - Defines
POST /api/v1/delivery-reports
as a basic webhook receiver (implementation details in Section 8). This endpoint is not rate-limited by default, as traffic comes from Plivo IPs.
- Defines
- Rate Limiter: Uses
src/app.js
)
4. Setting up the Express Application (This file ties everything together: initializes Express, loads middleware, mounts the API routes, and starts the server.
// src/app.js
const express = require('express');
const dotenv = require('dotenv');
const apiRoutes = require('./routes/api');
const logger = require('./utils/logger');
// Load environment variables from .env file FIRST
dotenv.config();
// Now, check for critical env vars before proceeding further
// (plivoService also checks, but this ensures Express doesn't start without them)
if (!process.env.PLIVO_AUTH_ID || !process.env.PLIVO_AUTH_TOKEN || !process.env.PLIVO_SENDER_ID) {
logger.error('FATAL: Missing required Plivo credentials in environment variables. Shutting down.');
process.exit(1);
}
const app = express();
const port = process.env.PORT || 3000;
// --- Middleware ---
// Enable parsing of JSON request bodies (for /send-bulk)
app.use(express.json({ limit: '10mb' })); // Increase limit if expecting very large recipient lists
// Enable parsing of URL-encoded request bodies (needed for Plivo webhooks)
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Basic request logging middleware
app.use((req, res, next) => {
logger.info(`Incoming Request: ${req.method} ${req.originalUrl} - IP: ${req.ip}`);
const start = process.hrtime();
res.on('finish', () => {
const diff = process.hrtime(start);
const responseTime = (diff[0] * 1e3 + diff[1] * 1e-6).toFixed(3); // milliseconds
logger.info(`Request Handled: ${res.statusCode} ${res.statusMessage} - ${req.method} ${req.originalUrl} - Response Time: ${responseTime}ms`);
});
next();
});
// --- API Routes ---
app.use('/api/v1', apiRoutes); // Mount API routes under /api/v1 prefix
// --- Health Check Endpoint ---
app.get('/health', (req, res) => {
// Basic health check confirms the server is running
res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});
// --- Not Found Handler ---
// Catch 404s for routes not defined
app.use((req, res, next) => {
res.status(404).json({ success: false, error: 'Not Found' });
});
// --- Global Error Handler ---
// Catch-all for errors passed via next(err) or thrown in async routes
// Must have 4 arguments (err, req, res, next) to be recognized as an error handler
app.use((err, req, res, next) => {
logger.error('Unhandled error:', {
message: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip
});
// Avoid leaking stack traces in production
const statusCode = err.status || err.statusCode || 500; // Use error's status if available
res.status(statusCode).json({
success: false,
error: (process.env.NODE_ENV === 'production' && statusCode === 500)
? 'Internal Server Error'
: err.message || 'An unexpected error occurred',
// Optionally include stack trace in development only
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
});
});
// --- Start Server ---
// The check for credentials is now done at the top
app.listen(port, () => {
logger.info(`Server listening on port ${port}`);
logger.info(`Bulk SMS API available at http://localhost:${port}/api/v1/send-bulk`);
if (process.env.WEBHOOK_URL) {
logger.info(`Delivery Report endpoint configured at http://localhost:${port}/api/v1/delivery-reports`);
} else {
logger.warn('Delivery Report endpoint URL (WEBHOOK_URL) not set in .env - Delivery status tracking is disabled.');
}
});
module.exports = app; // Export for potential testing
- Credential Check: Critical check for Plivo credentials moved to the top after
dotenv.config()
to ensure the application exits immediately if they are missing. - Middleware: Includes
express.json()
and cruciallyexpress.urlencoded({ extended: true })
which is needed to parse the defaultapplication/x-www-form-urlencoded
format used by Plivo webhooks. Request logging middleware is included. - Routes: Mounts the API routes from
src/routes/api.js
. - Health Check: Basic
/health
endpoint. - Error Handling: Includes a 404 handler for undefined routes and a global error handler that logs errors and sends appropriate responses (hiding stack traces in production).
- Server Start: Starts the Express server, logging the API and potentially the webhook endpoint URL.
5. Running and Testing the Application
-
Start the Server: From your project's root directory (
plivo-bulk-sms/
), run:node src/app.js
You should see log output indicating the server is running and the Plivo client initialized. If credentials were missing, it should have logged an error and exited.
-
Test with
curl
(or Postman): Open a new terminal window and usecurl
to send a POST request to your API endpoint.- Replace placeholders:
- Use valid phone numbers in E.164 format (e.g.,
+15551234567
,+447700900123
). - If using a Plivo Trial account, these numbers must be verified sandbox numbers added in your Plivo Console under Phone Numbers > Sandbox Numbers.
- Adjust the message text as desired.
- Use valid phone numbers in E.164 format (e.g.,
curl -X POST http://localhost:3000/api/v1/send-bulk \ -H ""Content-Type: application/json"" \ -d '{ ""recipients"": [ ""+15551234567"", ""+14155550001"", ""+447700900123"" ], ""message"": ""Hello from your Plivo Bulk Sender! (Test)"" }'
- Replace placeholders:
-
Expected Response (Success Example): If successful, you should receive a response similar to this (details might vary):
{ ""success"": true, ""message"": ""Processed 1 batches. 1 succeeded, 0 failed."", ""details"": { ""success"": [ { ""number"": ""+15551234567"", ""apiResponse"": { ""message"": ""message(s) queued"", ""messageUuid"": [ ""xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1"", ""xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx2"", ""xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx3"" ], ""apiId"": ""xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx0"" } }, { ""number"": ""+14155550001"", ""apiResponse"": { /* ... same API response ... */ } }, { ""number"": ""+447700900123"", ""apiResponse"": { /* ... same API response ... */ } } ], ""failed"": [], ""batchesAttempted"": 1, ""batchesSucceeded"": 1, ""batchesFailed"": 0 } }
-
Expected Response (Partial Failure Example): If one batch failed (e.g., due to a temporary Plivo issue):
{ ""success"": false, // Indicates not all batches succeeded ""message"": ""Processed 2 batches. 1 succeeded, 1 failed."", ""details"": { ""success"": [ // Numbers from the successful batch { ""number"": ""+15551234567"", ""apiResponse"": { /* ... */ } } /* ... other successful numbers ... */ ], ""failed"": [ // Numbers from the failed batch { ""number"": ""+447700900999"", ""error"": ""Plivo API error message here"" }, /* ... other failed numbers ... */ ], ""batchesAttempted"": 2, ""batchesSucceeded"": 1, ""batchesFailed"": 1 } }
(Status code would be
207 Multi-Status
) -
Check Logs: Monitor the terminal where the server is running (
node src/app.js
). You should see logs for incoming requests, batch processing attempts, Plivo API responses, and any errors. Also check the Plivo Console under Logs > SMS for message records.
6. Implementing Retries (Basic Example)
Network glitches or temporary service issues can cause API calls to fail. Implementing a simple retry mechanism can improve reliability. We can add this to the plivoService.js
.
Note: Sophisticated retry logic often involves exponential backoff, jitter, and considers more specific error types (e.g., 5xx vs 4xx). This is a basic illustration.
// src/services/plivoService.js (Modified section)
// ... (imports, config, client init remain the same) ...
const MAX_RETRIES = 2; // Number of retries per batch (total attempts = 1 + MAX_RETRIES)
const RETRY_DELAY_MS = 1000; // Delay between retries (simple fixed delay)
// Helper function for delayed execution
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// --- Core Bulk Send Function (with Retries) ---
const sendBulkSms = async (recipients, messageText) => {
if (!client) { /* ... client check ... */ }
if (!recipients || recipients.length === 0) { /* ... recipient check ... */ }
if (!messageText) { /* ... message check ... */ }
logger.info(`Starting bulk SMS send job for ${recipients.length} recipients with up to ${MAX_RETRIES} retries per batch.`);
const results = {
success: [],
failed: [],
batchesAttempted: 0,
batchesSucceeded: 0,
batchesFailed: 0,
};
for (let i = 0; i < recipients.length; i += bulkLimit) {
const batch = recipients.slice(i_ i + bulkLimit);
const destinationNumbers = batch.join('<');
results.batchesAttempted++;
const batchNumber = results.batchesAttempted; // For clearer logging
logger.debug(`Processing batch ${batchNumber}: ${batch.length} numbers.`);
let attempts = 0;
let batchSuccess = false;
let lastError = null;
while (attempts <= MAX_RETRIES && !batchSuccess) {
attempts++;
if (attempts > 1) {
logger.warn(`Retrying batch ${batchNumber}, attempt ${attempts} of ${MAX_RETRIES + 1} after ${RETRY_DELAY_MS}ms delay.`);
await delay(RETRY_DELAY_MS);
}
try {
const params = { src: senderId, dst: destinationNumbers, text: messageText };
if (webhookUrl) params.url = webhookUrl;
const response = await client.messages.create(params);
logger.info(`Plivo API response for batch ${batchNumber} (attempt ${attempts}): ${JSON.stringify(response)}`);
// Code continues here... (Original text was cut off)
// Assuming the intention was to push successful numbers after a successful attempt:
results.success.push(...batch.map(num => ({ number: num, apiResponse: response, attempt: attempts })));
results.batchesSucceeded++; // Increment overall batch success count
batchSuccess = true; // Mark batch as successful to exit retry loop
} catch (error) {
lastError = error; // Store the last error encountered for this batch
logger.error(`Attempt ${attempts} failed for batch ${batchNumber}: ${error.message}`, { stack: error.stack });
// If this was the last attempt, record the failure
if (attempts > MAX_RETRIES) {
logger.error(`Batch ${batchNumber} failed after ${MAX_RETRIES + 1} attempts. Recording failures.`);
results.failed.push(...batch.map(num => ({ number: num, error: lastError.message })));
results.batchesFailed++; // Increment overall batch failure count
}
}
} // End while loop (retry attempts)
} // End for loop (batches)
logger.info(`Bulk SMS job finished. ${results.batchesSucceeded} batches succeeded, ${results.batchesFailed} batches failed.`);
return results;
};
module.exports = {
sendBulkSms,
};