Build a Bulk SMS Broadcaster with Plivo, Fastify & Node.js: Complete Guide (2025)
Learn how to build a scalable bulk SMS broadcasting service using Plivo's API, Fastify web framework, and Node.js. Includes setup, error handling, retry logic, and deployment best practices.
Code examples20 min read
Frequently asked questions
Create a Fastify API endpoint that accepts an array of destination phone numbers and a message body. This endpoint uses the Plivo Node.js SDK to send a single API request to Plivo, broadcasting the SMS to all recipients simultaneously. This method improves efficiency compared to sending individual messages.
Plivo's bulk messaging API allows sending a single message to thousands of recipients with one API call, reducing latency and overhead compared to looping through individual messages. This approach is crucial for efficient and scalable SMS broadcasting.
Fastify is a high-performance Node.js web framework known for its speed and extensibility. Its efficiency minimizes overhead, making it ideal for handling the demands of a high-throughput SMS broadcasting service.
Use bulk SMS when sending the same message to many recipients. This method is significantly more efficient and prevents hitting API rate limits, especially when dealing with thousands of numbers. Individual messages are better for personalized communications.
Use npm install fastify plivo dotenv fastify-env to install the required packages. For development, add npm install --save-dev nodemon pino-pretty for automatic server restarts and readable logs. Remember to replace placeholders with your actual Plivo Auth ID, Token, and phone number.
dotenv loads environment variables from a .env file, which stores sensitive credentials like your Plivo Auth ID and Token. fastify-env adds schema-based validation for these environment variables, ensuring required values are present and correctly formatted.
Create directories for routes (src/routes), services (src/services), and a main server file (src/server.js). The routes define API endpoints, the services encapsulate interaction with Plivo, and the server file sets up the application. Add a .env file for credentials and a .gitignore for security best practices.
You need Node.js v18 or later, npm or yarn, a Plivo account, a Plivo phone number enabled for SMS, and your Plivo Auth ID and Auth Token. Remember to keep these credentials secure and never commit them to version control.
Implement detailed logging at both the service and route layers, including specific error messages from Plivo. For enhanced resilience, integrate retry mechanisms with exponential backoff for transient errors like network issues or rate limiting, using libraries like async-retry.
Create a .env file in your project root. Add your PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, and PLIVO_SOURCE_NUMBER (in E.164 format). Never commit this file to version control. Use environment variable management tools provided by your hosting platform for production environments.
The sendBulkSms function uses a Set to automatically remove duplicate phone numbers from the provided list before sending the bulk message, ensuring that each recipient receives the SMS only once. This prevents sending unnecessary duplicates and is handled internally.
The PLIVO_SOURCE_NUMBER is your Plivo phone number that will be used to send the bulk SMS messages. You can find it in your Plivo console under Messaging -> Phone Numbers. Ensure this number is enabled for SMS and is in E.164 format (e.g. +14155552671).
Yes, Plivo supports sending SMS to international numbers, but ensure your Plivo phone number is set up to send to your target regions. When adding numbers to your list, make sure they are in international E.164 formatting. Consider checking Plivo's documentation for regional regulations and number formatting.
The .env file stores sensitive credentials like API keys and tokens. Committing it to Git poses a security risk, as it could expose those credentials to unauthorized users, including if your repository is public or shared. It is crucial for security to add .env to your .gitignore.
One API. Every channel.
Contact Sales
Build a scalable bulk SMS broadcasting service using Fastify's performance and Plivo's efficient bulk messaging API. Send a single SMS message to thousands of recipients with a single API request. This guide walks you through project setup, implementation, error handling, retry logic, and deployment best practices.
You'll build a robust API endpoint that accepts phone numbers and messages, then uses Plivo to broadcast SMS efficiently. This approach dramatically outperforms sending individual messages in a loop – reducing latency and API call overhead.
Project Overview and Goals
Problem: Sending the same SMS message to numerous recipients individually is slow, inefficient, and easily hits API rate limits.
Solution: Build a Fastify API endpoint (
POST /broadcast) that accepts destination phone numbers and a message body. Your endpoint will use the Plivo Node.js SDK to send messages to all recipients via Plivo's bulk messaging feature (single API call).Technologies:
dotenv&fastify-env: Manage environment variables securely and reliably.Architecture:
Outcome: A functional Fastify application with a single endpoint (
/broadcast) that accepts bulk SMS requests and dispatches them via Plivo.Prerequisites:
1. Setting Up the Project
Initialize your Node.js project and install dependencies. These instructions work across all operating systems.
Create Project Directory:
mkdir fastify-plivo-broadcaster cd fastify-plivo-broadcasterInitialize npm Project:
This creates a
package.jsonfile.Install Dependencies:
fastify: The core web framework.plivo: The official Plivo Node.js SDK.dotenv: Loads environment variables from a.envfile.fastify-env: Schema-based environment variable validation for Fastify.Install Development Dependencies (Optional but Recommended):
nodemon: Automatically restarts the server on file changes during development.pino-pretty: Formats Pino logs for readability during development.Configure
package.jsonScripts:Open
package.jsonand add/modify thescriptssection:{ ""name"": ""fastify-plivo-broadcaster"", ""version"": ""1.0.0"", ""description"": """", ""main"": ""src/server.js"", ""scripts"": { ""start"": ""node src/server.js"", ""dev"": ""nodemon src/server.js | pino-pretty"", ""test"": ""echo \""Error: no test specified\"" && exit 1"" }, ""keywords"": [], ""author"": """", ""license"": ""ISC"", ""dependencies"": { ""dotenv"": ""^..."", ""fastify"": ""^..."", ""fastify-env"": ""^..."", ""plivo"": ""^..."" }, ""devDependencies"": { ""nodemon"": ""^..."", ""pino-pretty"": ""^..."" } }(Note: The
devscript pipes output topino-prettyfor readable logs during development).Create Project Structure:
Organize your code for clarity and maintainability.
mkdir src mkdir src/routes mkdir src/services touch src/server.js touch src/routes/broadcast.js touch src/services/plivoService.js touch .env touch .gitignoresrc/: Contains all source code.src/routes/: Holds route definitions.src/services/: Contains business logic interacting with external services (like Plivo).src/server.js: The main application entry point..env: Stores sensitive credentials and configuration (DO NOT commit this file)..gitignore: Specifies files/directories to be ignored by Git.Configure
.gitignore:Add the following lines to your
.gitignorefile to prevent committing sensitive information and unnecessary files:Set Up Environment Variables (
.env):Open the
.envfile and add your Plivo credentials and server configuration.PLIVO_AUTH_ID: Your Plivo Account Auth ID. Find this on the overview page of your Plivo Console.PLIVO_AUTH_TOKEN: Your Plivo Account Auth Token. Also on the overview page.PLIVO_SOURCE_NUMBER: A Plivo phone number you own (in E.164 format, e.g.,+14155552671) enabled for SMS. Find this under Phone Numbers -> Your Numbers in the console.HOST: The host address the server should listen on (e.g.,0.0.0.0to listen on all available network interfaces, or127.0.0.1for localhost only).PORT: The port number for the server (e.g.,3000).LOG_LEVEL: Controls logging verbosity (e.g.,info,debug,warn,error). Fastify uses Pino logger.Replace the placeholder values with your actual Plivo credentials and desired settings.
Initialize Basic Fastify Server (
src/server.js):Set up the basic server structure and configure
fastify-envto load and validate our environment variables.// src/server.js 'use strict'; // Load environment variables from .env file first require('dotenv').config(); const Fastify = require('fastify'); const fastifyEnv = require('fastify-env'); const broadcastRoutes = require('./routes/broadcast'); // Define schema for environment variables const envSchema = { type: 'object', required: [ 'PORT', 'HOST', 'PLIVO_AUTH_ID', 'PLIVO_AUTH_TOKEN', 'PLIVO_SOURCE_NUMBER', ], properties: { PORT: { type: 'string', default: 3000 }, HOST: { type: 'string', default: '0.0.0.0' }, PLIVO_AUTH_ID: { type: 'string' }, PLIVO_AUTH_TOKEN: { type: 'string' }, PLIVO_SOURCE_NUMBER: { type: 'string', pattern: '^\\+[1-9]\\d{1,14}$' // E.164 format regex }, LOG_LEVEL: { type: 'string', default: 'info' }, // Add other env vars here if needed }, }; // Determine logger options based on NODE_ENV // Use pino-pretty only in development const loggerConfig = process.env.NODE_ENV === 'production' ? { level: process.env.LOG_LEVEL || 'info' } // Production: JSON logs : { // Development: Pretty logs level: process.env.LOG_LEVEL || 'info', transport: { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname', }, }, }; // Initialize Fastify with logging options const fastify = Fastify({ logger: loggerConfig }); const startServer = async () => { try { // Register fastify-env plugin await fastify.register(fastifyEnv, { schema: envSchema, dotenv: true, // Load .env file using dotenv }); // Make Plivo credentials available across the app via decoration // This prevents needing to pass `fastify.config` everywhere fastify.decorate('plivoConfig', { authId: fastify.config.PLIVO_AUTH_ID, authToken: fastify.config.PLIVO_AUTH_TOKEN, sourceNumber: fastify.config.PLIVO_SOURCE_NUMBER, }); // Register application routes fastify.register(broadcastRoutes, { prefix: '/api/v1' }); // Add a version prefix // Basic health check route fastify.get('/health', async (request, reply) => { return { status: 'ok', timestamp: new Date().toISOString() }; }); // Start the server await fastify.listen({ port: parseInt(fastify.config.PORT, 10), host: fastify.config.HOST, }); fastify.log.info(`Server dependencies loaded successfully.`); } catch (err) { fastify.log.error(err); process.exit(1); } }; startServer();dotenvto load the.envfile before Fastify initializes fully.fastify-envvalidates the loaded environment variables againstenvSchema. This ensures critical configuration is present and correctly formatted (like the E.164 pattern for the phone number).fastifyinstance withplivoConfigto make credentials easily accessible in services/routes without explicit passing./healthendpoint is added for monitoring./api/v1prefix for better API versioning.pino-prettyis conditionally used only whenNODE_ENVis notproduction, ensuring readable logs in development and efficient JSON logs in production.Run the Server (Development):
You should see pretty-printed output indicating the server is listening on the configured host and port (e.g.,
http://0.0.0.0:3000). Iffastify-envencounters missing or invalid variables, it will throw an error. To run in production mode (for JSON logs):NODE_ENV=production npm start.2. Implementing Core Functionality (Plivo Service)
Now, let's create the service responsible for interacting with the Plivo API.
Create Plivo Service (
src/services/plivoService.js):This service initializes the Plivo client and exposes a function to send bulk messages.
// src/services/plivoService.js 'use strict'; const plivo = require('plivo'); let plivoClient; // Singleton client instance /** * Initializes the Plivo client using configuration from the Fastify instance. * Should be called once during application startup or on first use. * @param {object} config - Plivo configuration object { authId, authToken, sourceNumber } * @param {object} logger - Fastify logger instance */ function initializePlivoClient(config, logger) { if (!plivoClient) { if (!config || !config.authId || !config.authToken) { logger.error('Plivo Auth ID or Auth Token is missing in config.'); throw new Error('Plivo credentials are required for initialization.'); } try { plivoClient = new plivo.Client(config.authId, config.authToken); logger.info('Plivo client initialized successfully.'); } catch (error) { logger.error({ err: error }, 'Failed to initialize Plivo client'); throw error; // Re-throw to prevent application start if client fails } } return plivoClient; } /** * Sends a single SMS message to multiple recipients using Plivo's bulk messaging. * @param {string[]} destinations - An array of destination phone numbers in E.164 format. * @param {string} message - The text message content. * @param {object} config - Plivo configuration object { authId, authToken, sourceNumber } * @param {object} logger - Fastify logger instance * @returns {Promise<object>} - The Plivo API response object. * @throws {Error} - If Plivo API call fails or input is invalid. */ async function sendBulkSms(destinations, message, config, logger) { // Ensure client is initialized (idempotent) initializePlivoClient(config, logger); if (!Array.isArray(destinations) || destinations.length === 0) { logger.warn('sendBulkSms called with invalid or empty destinations array.'); throw new Error('Destinations array cannot be empty.'); } if (!message || typeof message !== 'string' || message.trim() === '') { logger.warn('sendBulkSms called with invalid or empty message.'); throw new Error('Message content cannot be empty.'); } if (!config.sourceNumber) { logger.error('Plivo source number is missing from configuration.'); throw new Error('Plivo source number is required.'); } // Plivo expects destination numbers separated by '<' // Filter out potential duplicates before joining const uniqueDestinations = [...new Set(destinations)]; const plivoDestinationString = uniqueDestinations.join('<'); // Plivo's current limit is 1,000 unique destination numbers per API call const MAX_RECIPIENTS_PER_REQUEST = 1000; // Source: https://www.plivo.com/docs/messaging/api/message/bulk-messaging if (uniqueDestinations.length > MAX_RECIPIENTS_PER_REQUEST) { logger.warn(`Attempted to send to ${uniqueDestinations.length} recipients, exceeding Plivo's limit of ${MAX_RECIPIENTS_PER_REQUEST}. The current implementation does NOT automatically batch requests. Request may fail.`); // Depending on requirements, you might throw an error or implement batching logic here. // throw new Error(`Cannot send to more than ${MAX_RECIPIENTS_PER_REQUEST} recipients in a single request without batching.`); } logger.info( `Attempting to send bulk SMS via Plivo to ${uniqueDestinations.length} unique recipients.`, ); try { const response = await plivoClient.messages.create( config.sourceNumber, // src: Your Plivo number plivoDestinationString, // dst: Recipients separated by '<' message, // text: The message content // Optional parameters (e.g., method, url for status callbacks) // { // method: 'POST', // Method for status callbacks // url: 'https://your-app.com/plivo/status-callback' // Your callback URL // } ); logger.info( { message_uuid: response.messageUuid, api_id: response.apiId, recipient_count: uniqueDestinations.length, }, 'Plivo bulk message request successful.', ); // The response contains message UUIDs for tracking, but not individual delivery status here. // Use Webhooks for detailed status updates. return response; } catch (error) { logger.error( { err: error, plivoError: error.message, // Plivo SDK often puts useful info here statusCode: error.statusCode, // Plivo SDK might add statusCode }, 'Plivo API error during bulk message send.', ); // Re-throw a generic error or a custom application error throw new Error(`Failed to send bulk SMS via Plivo: ${error.message}`); } } module.exports = { initializePlivoClient, // Export initializer if needed elsewhere, though route will handle it sendBulkSms, };plivoClientto act as a singleton – the client is initialized only once.initializePlivoClientfunction handles the creation, taking config and logger from Fastify.sendBulkSmsperforms input validation (non-empty destinations array, non-empty message).new Set(). Crucially, it joins the unique destination numbers with the<delimiter as required by Plivo's bulk API. Plivo automatically verifies that each destination number is in the correct format and removes duplicates, but pre-filtering improves efficiency.plivoClient.messages.createwith the source number, the<-separated destination string, and the message text.urlparameter for status callbacks – implementing webhooks is a vital next step for production monitoring but beyond this initial setup scope.3. Building the API Layer (Broadcast Route)
Let's create the Fastify route that exposes our bulk sending functionality.
Define Route and Schema (
src/routes/broadcast.js):This file defines the
POST /broadcastendpoint, validates the incoming request body, and calls the Plivo service.// src/routes/broadcast.js 'use strict'; const { sendBulkSms, initializePlivoClient } = require('../services/plivoService'); // Define the schema for the request body and response const broadcastSchema = { body: { type: 'object', required: ['destinations', 'message'], properties: { destinations: { type: 'array', minItems: 1, items: { type: 'string', // Basic E.164 format validation pattern: '^\\+[1-9]\\d{1,14}$', }, description: 'Array of recipient phone numbers in E.164 format (e.g., +14155552671)', }, message: { type: 'string', minLength: 1, maxLength: 1600, // Standard SMS limit consideration (Plivo handles segmentation) description: 'The text message content to send.', }, }, }, response: { 200: { // Successful response type: 'object', properties: { message: { type: 'string' }, message_uuid: { type: 'array', items: { type: 'string' } }, // Plivo returns UUIDs array api_id: { type: 'string' }, recipient_count: { type: 'integer' }, }, }, 400: { // Bad request (validation failed) type: 'object', properties: { statusCode: { type: 'integer' }, error: { type: 'string' }, message: { type: 'string' }, }, }, 500: { // Server error (e.g., Plivo API failed) type: 'object', properties: { statusCode: { type: 'integer' }, error: { type: 'string' }, message: { type: 'string' }, }, }, // Add other status codes like 429 for Rate Limiting if implemented }, }; async function broadcastRoutes(fastify, options) { // Ensure Plivo client is initialized when this route plugin is loaded // Access config decorated onto fastify instance in server.js initializePlivoClient(fastify.plivoConfig, fastify.log); fastify.post('/broadcast', { schema: broadcastSchema }, async (request, reply) => { const { destinations, message } = request.body; const requestId = request.id; // Fastify generates a unique ID for each request fastify.log.info( `[${requestId}] Received broadcast request for ${destinations.length} destinations.`, ); try { // Call the Plivo service function const plivoResponse = await sendBulkSms( destinations, message, fastify.plivoConfig, // Pass Plivo config from fastify instance request.log // Pass request-specific logger for better context ); // Send success response back to the client reply.code(200).send({ message: plivoResponse.message, // e.g., ""message(s) queued"" message_uuid: plivoResponse.messageUuid, api_id: plivoResponse.apiId, recipient_count: destinations.length, // Or use unique count if preferred }); fastify.log.info( `[${requestId}] Broadcast request processed successfully. Plivo API ID: ${plivoResponse.apiId}`, ); } catch (error) { fastify.log.error( `[${requestId}] Error processing broadcast request: ${error.message}`, { err: error } // Log the full error object ); // Determine appropriate status code (could refine based on error type) // If it's a known Plivo client-side error (e.g., validation) maybe 400? // Otherwise, assume 500 for server/Plivo issues. // For now, defaulting to 500 for simplicity. reply.code(500).send({ statusCode: 500, error: 'Internal Server Error', message: `Failed to send broadcast: ${error.message}`, // Provide some context }); } }); // You can add more routes here if needed } module.exports = broadcastRoutes;broadcastSchemausing Fastify's schema validation capabilities. This automatically validates therequest.bodyagainst the defined structure, types, and constraints (likeminItems,minLength, and the E.164pattern). If validation fails, Fastify automatically sends a 400 Bad Request response.destinationsandmessagefrom the validatedrequest.body.sendBulkSmsservice function, passing the necessary data, the Plivo configuration (fastify.plivoConfig), and the request-specific logger (request.log) for contextual logging.Register Route in Server:
Ensure the route is registered in
src/server.js(already done in Step 1.9). The linefastify.register(broadcastRoutes, { prefix: '/api/v1' });handles this.Test the Endpoint:
Restart your server (
npm run dev). You can now test the endpoint usingcurlor a tool like Postman.Using
curl:Replace placeholders with your actual server address, valid E.164 numbers (use your own test numbers!), and a message.
curl -X POST http://localhost:3000/api/v1/broadcast \ -H ""Content-Type: application/json"" \ -d '{ ""destinations"": [""+14155551234"", ""+14155555678"", ""+14155551234""], ""message"": ""Hello from the Fastify Bulk Broadcaster! Test message."" }'Expected Success Response (Example):
{ ""message"": ""message(s) queued"", ""message_uuid"": [ ""e8f3a0ee-a1d7-11eb-8f9c-0242ac110002"", ""e8f3a3a8-a1d7-11eb-8f9c-0242ac110002"" ], ""api_id"": ""f9b3aabc-a1d7-11eb-b3e0-0242ac110003"", ""recipient_count"": 3 }(Note: Plivo's actual response structure for
message_uuidmight vary slightly; consult their documentation. The service filters duplicates before sending, so only 2 unique numbers are sent here).Example Error Response (Validation Failure):
If you send invalid data (e.g., missing
messageor invalid phone number format):{ ""statusCode"": 400, ""error"": ""Bad Request"", ""message"": ""body/destinations/0 must match pattern \""^\\\\+[1-9]\\\\d{1,14}$\"""" }Example Error Response (Server/Plivo Error):
If Plivo credentials are wrong or the API call fails:
{ ""statusCode"": 500, ""error"": ""Internal Server Error"", ""message"": ""Failed to send broadcast: Error: Authentication credentials invalid. Please check your auth_id and auth_token."" }4. Integrating with Plivo (Credentials & Setup)
We've already integrated the SDK, but let's explicitly cover obtaining and securing credentials.
Obtain Plivo Auth ID and Auth Token:
Obtain Plivo Source Number:
+14155552671).Secure Storage:
As done in Step 1.8, place these credentials only in your
.envfile:Crucially: Ensure
.envis listed in your.gitignorefile to prevent accidentally committing secrets to version control.In production environments, use your hosting provider's mechanism for managing secrets (e.g., AWS Secrets Manager, Kubernetes Secrets, environment variables injected by the platform).
Fallback Mechanisms:
5. Error Handling, Logging, and Retry Mechanisms
We've built basic error handling and logging. Let's enhance it.
Consistent Error Handling:
plivoService.js): Catches specific Plivo API errors, logs detailed information (including Plivo error messages/codes if available), and throws a standardizedErrorobject.broadcast.js): Catches errors propagated from the service layer. Logs the error with request context (request.id,request.log). Sends an appropriate HTTP status code (4xx for client errors it can detect, 5xx for server/dependency errors) and a generic error message to the client, avoiding leaking internal details. Fastify's schema validation automatically handles 400 errors for invalid input.fastify.setErrorHandler((error, request, reply) => { ... }). This can catch unexpected errors and ensure a consistent error response format.Logging:
LOG_LEVELenv var (info,debug,warn,error,fatal) to control verbosity.infois good for production,debugfor development.reqIdinto logs. We passrequest.logto the service layer (sendBulkSms) so service-level logs also contain the request ID, making it easy to trace a single request's lifecycle through logs.pino-prettyis used for development readability. In production (NODE_ENV=production), output defaults to JSON logs for easier parsing by log aggregation systems (e.g., ELK stack, Datadog, Loki).info)warnorinfo, Fastify handles response)info/debug)info)error)error)debug)Retry Mechanisms:
plivoClient.messages.create. Libraries likeasync-retryorp-retryare excellent for this.Example using
async-retry(Conceptual):First, install the library:
Then, modify the service conceptually:
// src/services/plivoService.js (Conceptual modification with async-retry) const retry = require('async-retry'); const plivo = require('plivo'); // Assuming plivo is already required let plivoClient; function initializePlivoClient(config, logger) { /* ... as before ... */ } async function sendBulkSms(destinations, message, config, logger) { initializePlivoClient(config, logger); // ... existing validation ... const uniqueDestinations = [...new Set(destinations)]; const plivoDestinationString = uniqueDestinations.join('<'); // ... MAX_RECIPIENTS_PER_REQUEST check ... logger.info( `Attempting to send bulk SMS via Plivo to ${uniqueDestinations.length} unique recipients (with retry logic).` ); try { // Wrap the Plivo call in retry logic const response = await retry( async (bail, attemptNumber) => { // bail is a function to stop retrying (e.g., for auth errors) // attemptNumber is the current retry attempt logger.debug(`Plivo API call attempt #${attemptNumber}`); try { const result = await plivoClient.messages.create( config.sourceNumber, plivoDestinationString, message // Optional params... ); logger.info(`Plivo API attempt #${attemptNumber} successful.`); return result; // Success, return result } catch (error) { logger.warn(`Plivo API attempt #${attemptNumber} failed: ${error.message}`); // Decide if the error is retryable // Example: Don't retry authentication errors (401) if (error.statusCode === 401 || (error.message && error.message.includes('Authentication credentials invalid'))) { logger.error('Authentication error. Not retrying.'); bail(new Error(`Plivo authentication failed: ${error.message}`)); // Stop retrying return; // Necessary after bail } // Example: Retry potentially transient errors (rate limit 429, server errors 5xx) if (error.statusCode === 429 || error.statusCode >= 500) { logger.warn(`Retryable error detected (status: ${error.statusCode}). Throwing to trigger retry.`); throw error; // Throw error to trigger retry by async-retry } // For other client-side errors (e.g., invalid number 400), don't retry logger.error(`Non-retryable Plivo error (status: ${error.statusCode || 'N/A'}). Not retrying.`); bail(new Error(`Non-retryable Plivo error: ${error.message}`)); return; // Necessary after bail } }, { retries: 3, // Number of retries (total attempts = retries + 1) factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial delay ms maxTimeout: 5000, // Max delay ms onRetry: (error, attempt) => { logger.warn(`Retrying Plivo API call (attempt ${attempt}) due to: ${error.message}`); }, } ); logger.info( { message_uuid: response.messageUuid, api_id: response.apiId, recipient_count: uniqueDestinations.length, }, 'Plivo bulk message request successful after potential retries.', ); return response; } catch (error) { // This catches errors after all retries fail or if bail was called logger.error( { err: error, plivoError: error.message, statusCode: error.statusCode, }, 'Plivo API error during bulk message send after all retries.', ); throw new Error(`Failed to send bulk SMS via Plivo after retries: ${error.message}`); } } module.exports = { initializePlivoClient, sendBulkSms };6. Deployment and Production Considerations
Before deploying your Fastify bulk SMS broadcaster to production, consider these important factors:
Environment Variables:
NODE_ENV=productionto enable production logging (JSON format, nopino-pretty).Security:
/broadcastendpoint to prevent abuse. Use plugins like@fastify/rate-limit.Monitoring and Observability:
/healthendpoint should be monitored by your orchestration platform (Kubernetes, AWS ECS, etc.).Webhooks for Delivery Status:
POST /api/v1/plivo/callback).Scaling Considerations:
Error Recovery:
Testing:
plivoService.js).Documentation:
Compliance:
Conclusion
You've successfully built a production-ready bulk SMS broadcasting service using Plivo, Fastify, and Node.js. This implementation provides:
Next Steps
Additional Resources