code examples
code examples
Plivo Bulk SMS with Next.js 15: Complete Implementation Guide
Learn how to build bulk SMS broadcasting with Plivo and Next.js 15. Send SMS to 1,000+ recipients using API batching, error handling, rate limiting, and production-ready Node.js code.
Build Bulk SMS Broadcasting with Plivo, Next.js 15, and Node.js
Building bulk SMS functionality in Next.js 15 with Plivo enables you to send messages to hundreds or thousands of recipients efficiently. This comprehensive guide walks you through implementing production-ready bulk SMS broadcasting using Plivo's Node.js SDK, including API batching for up to 1,000 recipients per request, authentication, error handling, rate limiting, and deployment strategies for serverless platforms like Vercel.
By the end of this tutorial, you'll have a Next.js application with a secure API endpoint capable of accepting a list of phone numbers and a message, then efficiently sending that message to all recipients via Plivo's bulk messaging capabilities. This solves the common need for applications to send notifications, alerts, or marketing messages to multiple users simultaneously without overwhelming the API or managing individual requests inefficiently.
Project Overview and Goals
-
Goal: Build a Next.js application featuring an API endpoint (
/api/send-bulk) that sends a single SMS message to multiple phone numbers using Plivo. -
Problem Solved: Provides a scalable and efficient way to broadcast messages, avoiding the complexity and potential rate-limiting issues of sending individual messages in a loop.
-
Technologies:
- Next.js 15: A popular React framework for building server-rendered and static web applications. You'll use its App Router feature with React 19 support.
- Plivo: A cloud communications platform providing SMS APIs. You'll use their Node.js SDK (npm package:
plivo). - Node.js: The runtime environment for Next.js and the Plivo SDK.
- (Optional) Prisma & PostgreSQL: For storing and retrieving contact lists (demonstrated but adaptable to other databases/ORMs).
-
Architecture:
text+-----------------+ +-----------------------+ +---------------------+ +-------------+ | User / Frontend | ---> | Next.js API Route | ---> | Plivo Node.js SDK | ---> | Plivo API | | (e.g., Admin UI)| | (/api/send-bulk) | | (Bulk Send Logic) | | (SMS Service)| +-----------------+ +-----------------------+ +---------------------+ +-------------+ | ^ | ^ | (Trigger Send) | | (Optional: Fetch Numbers) | | | v | | +---------|-----------------------+ | | | v | +-----------------+ | | Database | | | (e.g., Prisma) | +---------------------------| (Contact Lists) | +-----------------+(Note: An embedded image diagram would be more professional here if available.)
-
Prerequisites:
- Node.js v22 LTS or later (recommended as of 2025 for Active LTS support through October 2025)
- npm or yarn package manager
- A Plivo account (Sign up at Plivo.com)
- A Plivo phone number capable of sending SMS
- Basic understanding of JavaScript, React, and Next.js
- Access to a terminal or command prompt
1. Set Up Your Next.js Project with Plivo SDK
Initialize a new Next.js project and install the necessary dependencies.
-
Create Next.js App: Open your terminal and run:
bashnpx create-next-app@latest plivo-bulk-sms-app --typescript --eslint --tailwind --src-dir --app --import-alias "@/*"-
plivo-bulk-sms-app: Name your project differently if preferred. -
This command uses TypeScript, ESLint, Tailwind CSS, the
src/directory, and the App Router for a modern setup with Next.js 15. Adjust these flags if preferred. -
Navigate into the project directory:
bashcd plivo-bulk-sms-app
-
-
Install Plivo SDK: Add the official Plivo Node.js helper library.
bashnpm install plivo # or yarn add plivo- Note: Use the
plivopackage (not the legacyplivo-nodepackage which is deprecated). As of 2025, this is the actively maintained SDK.
- Note: Use the
-
Install Prisma (Optional, for Contact Management): If you plan to manage contacts in a database, set up Prisma.
bashnpm install prisma @prisma/client --save-dev # or yarn add prisma @prisma/client --dev npx prisma init --datasource-provider postgresql- This initializes Prisma and configures it for PostgreSQL. Change
postgresqlif you use a different database (e.g.,mysql,sqlite). - Update the
DATABASE_URLin the generated.envfile with your actual database connection string.
- This initializes Prisma and configures it for PostgreSQL. Change
-
Set Environment Variables: Create a file named
.env.localin the root of your project. Never commit this file to Git. Add your Plivo credentials and other sensitive configurations:dotenv# .env.local # Plivo Credentials (Get from Plivo Console > API Keys: https://console.plivo.com/dashboard/) PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN # Plivo Source Number (Must be a Plivo number enabled for SMS: https://console.plivo.com/numbers/) # Use E.164 format, e.g., +14155551212 PLIVO_SOURCE_NUMBER=+1XXXXXXXXXX # Internal API Key (Generate a strong random string for securing your API endpoint) INTERNAL_API_KEY=YOUR_STRONG_SECRET_API_KEY # Database URL (If using Prisma) # Example for PostgreSQL: postgresql://user:password@host:port/database?schema=public DATABASE_URL=YOUR_DATABASE_CONNECTION_STRING- Purpose:
PLIVO_AUTH_ID/PLIVO_AUTH_TOKEN: Your primary API credentials for authenticating your application with the Plivo API. Obtain these from your Plivo dashboard under "API Keys."PLIVO_SOURCE_NUMBER: The Plivo phone number that will appear as the sender of the SMS messages. Must be SMS-enabled and in E.164 format. Find your numbers under "Messaging" → "Numbers" in the Plivo console.INTERNAL_API_KEY: A secret key you'll use to protect your API endpoint from unauthorized access. Generate a secure, random string for this (e.g., usingopenssl rand -base64 32in your terminal).DATABASE_URL: The connection string for your database if you use Prisma. Format depends on the database provider.
- Purpose:
-
Project Structure: Your
srcdirectory looks like this initially:textsrc/ ├── app/ │ ├── api/ # API routes │ │ ├── send-bulk/ │ │ │ └── route.ts # Your bulk sending endpoint │ │ └── plivo-status-callback/ # Optional: For delivery reports │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx # Main frontend page (optional for this guide) ├── lib/ # Utility functions, Plivo client setup │ ├── plivo.ts │ ├── plivoService.ts # Core bulk sending logic │ ├── utils.ts # Helper functions (e.g., batching) │ ├── logger.ts # Optional: Structured logger │ └── prisma.ts # Optional: Prisma client instance ├── components/ # React components (optional for this guide) └── prisma/ # Prisma schema and migrations (if using) ├── migrations/ └── schema.prisma- Architectural Decision: Place API logic in
app/api/per Next.js App Router conventions. Shared logic like Plivo client initialization goes intolib/.
- Architectural Decision: Place API logic in
2. Implement Bulk SMS Logic with Plivo API
Plivo's API allows sending to multiple destinations in a single request by providing a <-delimited string of numbers in the dst parameter. Plivo supports up to 1,000 unique destination numbers per API request for bulk messaging (verified from official Plivo documentation, 2025). Batch your recipient list accordingly.
-
Create a Batching Utility: Build a helper function to split an array into chunks.
typescript// src/lib/utils.ts /** * Splits an array into chunks of a specified size. * @param array The array to split. * @param chunkSize The maximum size of each chunk. * @returns An array of chunks. */ export function chunkArray<T>(array: T[], chunkSize: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; }- Why: This function breaks down a large list of recipients into smaller batches that comply with Plivo's API limit of 1,000 recipients per request.
-
Set Up Plivo Client: Initialize the Plivo client using environment variables.
typescript// src/lib/plivo.ts import * as plivo from 'plivo'; const authId = process.env.PLIVO_AUTH_ID; const authToken = process.env.PLIVO_AUTH_TOKEN; if (!authId || !authToken) { throw new Error("Plivo credentials (PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN) are not set in environment variables."); } export const plivoClient = new plivo.Client(authId, authToken); // console.log("Plivo client initialized."); // Optional: Log initialization (consider using a proper logger in production)- Why: Centralizes Plivo client instantiation, making it reusable across your application. Reads credentials securely from environment variables. Includes a check to ensure credentials are set.
-
Build the Bulk Send Service Function: Create the core function that handles batching and sending.
typescript// src/lib/plivoService.ts (Create this new file) import { plivoClient } from './plivo'; import { chunkArray } from './utils'; import { MessageCreateResponse } from 'plivo/dist/resources/message'; // Import specific Plivo response type // Potentially import MessageCreateParams if using stricter typing for params // import { MessageCreateParams } from 'plivo/dist/resources/message'; const PLIVO_SOURCE_NUMBER = process.env.PLIVO_SOURCE_NUMBER; const MAX_RECIPIENTS_PER_REQUEST = 1000; // Plivo's documented limit (verified 2025) if (!PLIVO_SOURCE_NUMBER) { throw new Error("PLIVO_SOURCE_NUMBER is not set in environment variables."); } interface SendBulkSmsResult { success: boolean; batchResults: { batch: string[]; response?: MessageCreateResponse; // Plivo's response type for success error?: any; // Error details if a batch failed }[]; error?: string; // General error message if validation fails early } /** * Sends an SMS message to multiple recipients in batches using Plivo. * @param recipientNumbers An array of phone numbers in E.164 format. * @param message The text message content. * @param deliveryReportUrl Optional URL for receiving delivery status callbacks. * @returns A promise resolving to the results of all batch sends. */ export async function sendBulkSms( recipientNumbers: string[], message: string, deliveryReportUrl?: string ): Promise<SendBulkSmsResult> { if (!recipientNumbers || recipientNumbers.length === 0) { return { success: false, batchResults: [], error: "Recipient list is empty." }; } if (!message) { return { success: false, batchResults: [], error: "Message content is empty." }; } // Validate all numbers before starting the process const invalidNumbers = recipientNumbers.filter(num => !/^\+\d{10,15}$/.test(num)); if (invalidNumbers.length > 0) { console.error("Invalid E.164 numbers found:", invalidNumbers); return { success: false, batchResults: [], error: `Invalid E.164 numbers found: ${invalidNumbers.join(', ')}` }; } const numberBatches = chunkArray(recipientNumbers, MAX_RECIPIENTS_PER_REQUEST); const batchResults: SendBulkSmsResult['batchResults'] = []; let overallSuccess = true; console.log(`Sending message to ${recipientNumbers.length} recipients in ${numberBatches.length} batches (max ${MAX_RECIPIENTS_PER_REQUEST} per batch).`); for (const batch of numberBatches) { const destinationString = batch.join('<'); // Plivo's delimiter for bulk numbers const batchResult: SendBulkSmsResult['batchResults'][0] = { batch }; try { console.log(`Sending batch to: ${destinationString.substring(0, 50)}…`); // Log truncated dst // Using 'any' for flexibility, especially when conditionally adding 'url'. // Refine with specific Plivo types (e.g., MessageCreateParams from plivo/dist/resources/message) // if the structure is fixed and known. const params: any = { src: PLIVO_SOURCE_NUMBER, dst: destinationString, text: message, }; if (deliveryReportUrl) { params.url = deliveryReportUrl; // Add callback URL if provided params.method = 'POST'; // Recommended method for callbacks } const response = await plivoClient.messages.create(params); console.log(`Batch sent successfully. Plivo Response:`, response); batchResult.response = response; } catch (error: any) { console.error(`Error sending batch to: ${destinationString.substring(0, 50)}…`, error); batchResult.error = error.message || error; overallSuccess = false; // Mark overall success as false if any batch fails } batchResults.push(batchResult); // Optional: Add a small delay between batches to avoid hitting rate limits aggressively // await new Promise(resolve => setTimeout(resolve, 500)); // e.g., 500 ms delay } console.log(`Bulk send process completed. Overall Success: ${overallSuccess}`); return { success: overallSuccess, batchResults }; }- Why:
- Encapsulates the core logic for sending bulk SMS.
- Uses the
chunkArrayutility to divide recipients. - Formats the
dstparameter correctly using the<delimiter as required by Plivo's bulk feature. - Iterates through batches and calls
plivoClient.messages.createfor each. - Includes basic input validation and E.164 format check before batching.
- Provides structured results, indicating success/failure for each batch.
- Includes logging for better observability.
- Handles potential errors during API calls gracefully for each batch.
- Adds an optional
deliveryReportUrlparameter for status tracking (covered later). - Uses specific Plivo types (
MessageCreateResponse) for better code safety where applicable.
- Why:
3. Build Your Next.js API Route for SMS Broadcasting
Create the Next.js API route that exposes this functionality.
Note on Next.js 15 Changes: Next.js 15 introduces caching changes where GET Route Handlers and the Client Router Cache changed from cached by default to uncached by default. This guide focuses on POST routes for sending SMS, which are not cached.
Production Consideration – Rate Limiting: For production deployments, especially on serverless platforms like Vercel, implement rate limiting to prevent API abuse. Consider using @upstash/ratelimit (recommended in Next.js documentation for serverless environments) or alternatives like rate-limiter-flexible for Redis-based solutions. Rate limiting prevents excessive API calls, protects against DoS attacks, and controls costs.
// src/app/api/send-bulk/route.ts
import { NextResponse } from 'next/server';
import { sendBulkSms } from '@/lib/plivoService';
// import { prisma } from '@/lib/prisma'; // Uncomment if using Prisma
const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY;
// Define the expected structure of the request body
interface RequestBody {
numbers?: string[]; // List of E.164 phone numbers
// contactGroupId?: string; // Alternative: ID to fetch numbers from DB (requires Prisma setup)
message: string;
}
export async function POST(request: Request) {
console.log('Received request on /api/send-bulk');
// 1. Authentication
const authHeader = request.headers.get('Authorization');
if (!INTERNAL_API_KEY) {
console.error('INTERNAL_API_KEY is not set in environment variables.');
return NextResponse.json({ error: 'Internal Server Configuration Error' }, { status: 500 });
}
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.substring(7) !== INTERNAL_API_KEY) {
console.warn('Unauthorized attempt to access /api/send-bulk');
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
// 2. Request Body Parsing and Validation
const body: RequestBody = await request.json();
const { numbers, message /*, contactGroupId */ } = body;
if (!message || typeof message !== 'string' || message.trim() === '') {
return NextResponse.json({ error: 'Invalid request: message is required and must be a non-empty string.' }, { status: 400 });
}
let recipientNumbers: string[] = [];
// --- Option A: Direct Number List ---
if (numbers) {
if (!Array.isArray(numbers)) {
return NextResponse.json({ error: 'Invalid request: numbers must be an array.' }, { status: 400 });
}
// Basic E.164 format check – more robust check happens in plivoService
const potentiallyInvalid = numbers.filter(num => typeof num !== 'string' || !num.startsWith('+'));
if (potentiallyInvalid.length > 0) {
return NextResponse.json({ error: 'Invalid request: numbers must be an array of strings potentially in E.164 format (e.g., +12223334444).' }, { status: 400 });
}
recipientNumbers = numbers;
}
// --- Option B: Fetch from Database (Example using Prisma – requires setup in Section 6) ---
/*
else if (contactGroupId) {
console.log(`Fetching numbers for contact group ID: ${contactGroupId}`);
// Ensure Prisma client is available
// if (!prisma) {
// console.error('Prisma client is not available.');
// return NextResponse.json({ error: 'Database configuration error.' }, { status: 500 });
// }
try {
const groupWithContacts = await prisma.contactGroup.findUnique({
where: { id: contactGroupId },
include: { contacts: { select: { phone_number: true } } },
});
if (!groupWithContacts) {
return NextResponse.json({ error: `Contact group with ID ${contactGroupId} not found.` }, { status: 404 });
}
// Ensure phone numbers are valid before adding
recipientNumbers = groupWithContacts.contacts
.map(c => c.phone_number)
.filter(num => /^\+\d{10,15}$/.test(num)); // Validate here too
console.log(`Fetched ${recipientNumbers.length} valid numbers for group ${contactGroupId}`);
} catch (dbError: any) {
console.error("Database error fetching contact group:", dbError);
return NextResponse.json({ error: 'Failed to retrieve contacts from database.' }, { status: 500 });
}
}
*/
else {
// If neither numbers nor contactGroupId is provided (and contactGroupId logic is enabled)
return NextResponse.json({ error: 'Invalid request: Either "numbers" array or "contactGroupId" must be provided.' }, { status: 400 });
}
if (recipientNumbers.length === 0) {
return NextResponse.json({ error: 'No valid recipient numbers found or provided.' }, { status: 400 });
}
// 3. Call the Bulk Send Service
console.log(`Initiating bulk send to ${recipientNumbers.length} numbers.`);
// Construct the absolute URL for the delivery report callback (if needed)
// Ensure VERCEL_URL is set in your Vercel project settings for production/preview
const baseUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'http://localhost:3000';
const deliveryReportUrl = `${baseUrl}/api/plivo-status-callback`; // Define your callback endpoint
const result = await sendBulkSms(recipientNumbers, message.trim(), deliveryReportUrl);
// 4. Return Response
if (result.success) {
console.log('Bulk send successful.');
// Avoid sending detailed Plivo responses back to the client unless necessary
return NextResponse.json({ message: 'Bulk message sending process initiated successfully.', batchCount: result.batchResults.length }, { status: 200 });
} else {
console.error('Bulk send process completed with errors.');
// Log detailed batchResults errors for server-side debugging
// Consider what level of detail to return to the client
return NextResponse.json({
error: 'Bulk message sending process encountered errors.',
details: result.error, // General error or batch-specific errors summarized
failedBatchCount: result.batchResults.filter(r => r.error).length
}, { status: result.error?.includes("Invalid E.164") ? 400 : 500 }); // Return 400 for input errors, 500 otherwise
}
} catch (error: any) {
console.error('Error in /api/send-bulk handler:', error);
if (error instanceof SyntaxError) { // JSON parsing error
return NextResponse.json({ error: 'Invalid JSON payload.' }, { status: 400 });
}
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
// Optional: Add GET or other methods if needed, otherwise they default to 405 Method Not Allowed
export async function GET() {
return NextResponse.json({ error: 'Method Not Allowed' }, { status: 405 });
}-
Why:
- Authentication: Protects the endpoint using the
INTERNAL_API_KEYvia theAuthorization: Bearer <key>header. Checks if the key is configured. - Validation: Checks the request body for required fields (
message) and validates the format ofnumbers(array of strings, basic format check). Provides clear error messages for bad requests. - Data Fetching (Optional): Includes commented-out example logic for fetching numbers from a database using Prisma based on a
contactGroupId. Added comments about dependencies. - Service Call: Delegates the actual sending logic to the
sendBulkSmsfunction inplivoService.ts. - Response Handling: Returns appropriate HTTP status codes (200, 400, 401, 500) and JSON responses based on the outcome. Returns less detailed success/error info to the client for security/simplicity.
- Callback URL: Constructs an absolute URL for Plivo delivery reports, essential for tracking status (using Vercel environment variables or localhost).
- Authentication: Protects the endpoint using the
-
Test with
curl:bashcurl -X POST http://localhost:3000/api/send-bulk \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_STRONG_SECRET_API_KEY" \ -d '{ "numbers": ["+12223334444", "+15556667777"], "message": "Hello from your Next.js bulk sender! (Test)" }'(Replace
YOUR_STRONG_SECRET_API_KEYand use valid E.164 phone numbers like +12223334444 for testing)Expected Success Response (JSON):
json{ "message": "Bulk message sending process initiated successfully.", "batchCount": 1 }(Status Code: 200)
Expected Error Response (e.g., Unauthorized – JSON):
json{ "error": "Unauthorized" }(Status Code: 401)
Expected Error Response (e.g., Bad Request – JSON):
json{ "error": "Invalid request: message is required and must be a non-empty string." }(Status Code: 400)
4. Integrate Plivo SMS Service
This section focuses on the specifics of the Plivo integration itself.
-
Configuration: As covered in Section 1 (Setup), essential configuration happens via environment variables (
.env.local):PLIVO_AUTH_ID: Your Plivo Account Auth ID.PLIVO_AUTH_TOKEN: Your Plivo Account Auth Token.PLIVO_SOURCE_NUMBER: Your Plivo SMS-enabled number.- How to Obtain:
- Log in to your Plivo Console (https://console.plivo.com/).
- Auth ID & Token: Navigate to the main Dashboard page. Your Auth ID and Token appear prominently at the top right. Click the "eye" icon to reveal the token.
- Source Number: Navigate to "Messaging" → "Numbers." Ensure you have a number listed here. If not, rent one. Copy the number exactly as shown, including the
+and country code (E.164 format).
- Security: Store these only in
.env.local(or your deployment environment's secret management) and ensure.env.localis listed in your.gitignorefile.
-
SDK Initialization: Covered in Section 2 (Core Functionality) in
src/lib/plivo.ts. Theplivo.Clientinitializes using the environment variables. -
API Call: The core interaction occurs in
src/lib/plivoService.tswithin thesendBulkSmsfunction:typescript// Inside sendBulkSms function const response = await plivoClient.messages.create({ src: PLIVO_SOURCE_NUMBER, dst: destinationString, // The '<'-delimited string text: message, url: deliveryReportUrl, // Optional callback URL method: 'POST', // Recommended for callbacks // Other potential params from Plivo SDK if needed });src: Your Plivo sending number.dst: The crucial parameter for bulk sending – multiple E.164 numbers joined by<.text: The message content.url: The publicly accessible endpoint in your application where Plivo sends status updates (delivery reports) for each message sent in the batch.method: The HTTP method Plivo uses to call yoururl(POST is recommended as it sends data in the body).
-
Fallback Mechanisms: The current implementation relies solely on Plivo. For critical messages, true fallback involves:
- Monitoring: Actively check Plivo's status page or API health endpoints.
- Alternative Provider: Have a secondary SMS provider configured.
- Logic: If
plivoClient.messages.createfails consistently (e.g., multiple retries fail, or Plivo status indicates an outage), trigger sending via the alternative provider. This adds significant complexity and cost, usually reserved for high-availability requirements. This guide does not implement a fallback provider.
5. Implement Error Handling, Logging, and Retry Mechanisms
Robust error handling is crucial for a production system.
-
Error Handling Strategy:
- API Route (
route.ts): Usetry…catchblocks to capture errors during request processing, validation, database access (if applicable), and calls to thesendBulkSmsservice. Return appropriate HTTP status codes (4xx for client errors, 5xx for server errors) with informative JSON error messages. Check for specific error types (like JSON parsing errors). - Service Layer (
plivoService.ts): Usetry…catchwithin the loop for each batch send. This allows the process to continue even if one batch fails. Log errors for each failed batch and aggregate the results. Return a clear success/failure status along with detailed batch results. Perform input validation early. - Plivo Client (
plivo.ts): Throw an error on initialization if credentials are missing.
- API Route (
-
Logging:
-
Current: Uses basic
console.logandconsole.error. Sufficient for development. -
Production: Integrate a structured logging library like
pinoorwinston.bashnpm install pino pino-pretty # pino-pretty for development formatting # or yarn add pino pino-prettyCreate a logger instance:
typescript// src/lib/logger.ts (Example – create this file) import pino from 'pino'; const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { colorize: true } } // Pretty print in dev : undefined, // Default JSON in prod for log ingestion systems }); export default logger;Replace
console.log/errorwithlogger.info/warn/errorthroughout your application (e.g., inroute.ts,plivoService.ts). Structured logs (JSON format in production) are easier to parse, filter, and analyze with log management tools (e.g., Datadog, Logtail, Loki).- Log Levels: Use appropriate levels:
infofor routine operations (API calls, batch starts),warnfor potential issues (retries, unexpected conditions),errorfor failures (API errors, exceptions). - Log Content: Include relevant context like request IDs (if available), batch numbers, error messages, and Plivo API responses/errors.
- Log Levels: Use appropriate levels:
-
-
Retry Mechanisms:
-
Concept: Network issues or temporary Plivo problems might cause API calls to fail. Retrying improves reliability for transient errors.
-
Simple Retry Implementation: Modify the
sendBulkSmsbatch loop to include retries with backoff. (This replaces the simpletry/catchblock within the loop):typescript// Inside the for…of loop in sendBulkSms (replace the simple try/catch) const maxAttempts = 3; // Max attempts per batch let success = false; let response: MessageCreateResponse | undefined; let lastError: any; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { // logger.info({ batch, attempt }, `Attempt ${attempt} to send batch.`); // Use logger console.log(`Attempt ${attempt} for batch: ${destinationString.substring(0, 50)}…`); const params: any = { /* … as before … */ }; if (deliveryReportUrl) { /* … as before … */ } response = await plivoClient.messages.create(params); success = true; batchResult.response = response; // logger.info({ batch, attempt, response }, `Batch sent successfully on attempt ${attempt}.`); console.log(`Batch sent successfully on attempt ${attempt}. Plivo Response:`, response); break; // Exit retry loop on success } catch (error: any) { lastError = error; // logger.warn({ batch, attempt, error: error.message }, `Attempt ${attempt} failed for batch.`); console.warn(`Attempt ${attempt} failed for batch: ${destinationString.substring(0, 50)}… Error: ${error.message}`); if (attempt < maxAttempts) { const delay = 1000 * Math.pow(2, attempt - 1); // Exponential backoff (1 s, 2 s) // logger.info({ batch, attempt, delay }, `Waiting ${delay}ms before next attempt.`); console.log(`Waiting ${delay} ms before next attempt.`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // End of retry loop if (!success) { // logger.error({ batch, error: lastError?.message || lastError }, `Batch failed after ${maxAttempts} attempts.`); console.error(`Batch failed after ${maxAttempts} attempts: ${destinationString.substring(0, 50)}… Last Error: ${lastError?.message}`); batchResult.error = lastError?.message || lastError; overallSuccess = false; } batchResults.push(batchResult); // Push result regardless of success/failure -
Exponential Backoff: The example uses simple exponential backoff (1 s, 2 s). Adding jitter (randomness) to the delay can be beneficial in high-concurrency scenarios. Libraries like
async-retrycan simplify complex retry logic. -
Caveat: Be cautious with retries for sending messages. If a request partially succeeded or timed out but Plivo did process it, retrying could lead to duplicate messages. Check Plivo's API idempotency guarantees or implement your own request tracking using unique IDs if duplicates are critical to avoid. Plivo's
messageUuidhelps track results but doesn't prevent duplicates if the create call is retried after initial success but before the response is received.
-
-
Test Error Scenarios:
- Invalid Credentials: Temporarily change
PLIVO_AUTH_ID/PLIVO_AUTH_TOKENin.env.local. Expect Plivo errors (likely HTTP 401) caught inplivoService.ts. - Invalid Source Number: Use a non-Plivo number or incorrectly formatted number for
PLIVO_SOURCE_NUMBER. Expect Plivo errors. - Invalid Destination Number: Include poorly formatted numbers (e.g.,
+123) or numbers known to be invalid in thenumbersarray. Expect a 400 error from the API route due to validation, or specific Plivo errors/failure statuses in delivery reports if they pass initial validation but fail at Plivo. - Insufficient Funds: If your Plivo account balance is too low, expect Plivo API errors.
- Network Issues: Simulate network drops locally (e.g., disconnect Wi-Fi briefly during a
curlrequest) or use tools likeiptables(Linux) or network link conditioners (macOS) to introduce packet loss or latency. Test if retries handle transient failures. - Rate Limiting: Send many requests in quick succession (you might need a script for this). Observe if Plivo returns 429 Too Many Requests errors and how your application handles them (ideally, the retry logic with backoff should help).
- Invalid Credentials: Temporarily change
Frequently Asked Questions About Plivo Bulk SMS with Next.js
How many recipients can I send to in a single Plivo API request?
Plivo supports up to 1,000 unique destination numbers per API request for bulk messaging. To send to more recipients, batch your phone number list into chunks of 1,000 numbers. Use the < delimiter to separate phone numbers in the dst parameter (e.g., +14155551234<+14155555678<+14155559012). The batching utility in this guide automatically handles this chunking for you.
What Node.js version should I use for Plivo bulk SMS?
Use Node.js v22 LTS or later (recommended as of 2025). Node.js v22 has Active LTS support through October 2025 and provides the stability and security updates needed for production applications. This version is fully compatible with Next.js 15, Plivo SDK, and modern JavaScript/TypeScript features used in this guide.
How do I handle Plivo API rate limits in Next.js?
Implement rate limiting at your API route level using @upstash/ratelimit (recommended for serverless environments like Vercel) or rate-limiter-flexible for Redis-based solutions. Additionally, add delays between batch sends (e.g., 500 ms) and implement exponential backoff retry logic for failed requests. Monitor for 429 Too Many Requests responses from Plivo and adjust your sending cadence accordingly.
Can I use Plivo bulk SMS with Next.js App Router?
Yes, this guide is specifically designed for Next.js 15 with App Router. Create your API route in src/app/api/send-bulk/route.ts and use the POST export function to handle bulk SMS requests. The App Router provides better performance and is the recommended approach for new Next.js applications, with full support for React 19 and server-side rendering.
How do I secure my Plivo bulk SMS API endpoint?
Implement multiple security layers: (1) Use Bearer token authentication with a strong INTERNAL_API_KEY stored in environment variables, (2) validate all input data including phone number format (E.164), (3) implement rate limiting to prevent abuse, (4) never expose Plivo credentials to the client, (5) use HTTPS in production, and (6) consider IP whitelisting for admin-only endpoints. Store all sensitive credentials in .env.local (never commit to Git) or use your deployment platform's secret management.
What's the difference between Plivo's bulk SMS and individual sends?
Plivo's bulk SMS API allows you to send one message to up to 1,000 recipients in a single API call by using the < delimiter in the dst parameter. This is significantly more efficient than making 1,000 individual API calls: (1) reduces API requests and network overhead, (2) faster processing time, (3) lower latency, (4) easier to track batches, (5) avoids hitting rate limits from excessive individual requests. Use bulk sends for notifications, alerts, and broadcast campaigns.
How do I track delivery status for Plivo bulk messages?
Configure the url parameter in your Plivo API request to receive delivery status callbacks. Set up a callback endpoint (e.g., /api/plivo-status-callback) in your Next.js application. Plivo will POST delivery reports to this URL for each message sent. Parse the callback data to track delivery status (delivered, failed, queued), message UUID, error codes, and timestamps. Store this data in your database for analytics and debugging.
Can I use Prisma to manage contact lists for Plivo bulk SMS?
Yes, this guide includes optional Prisma integration. Set up Prisma with PostgreSQL (or your preferred database), create a Contact and ContactGroup schema, and fetch phone numbers by group ID in your API route. Validate phone numbers before sending and filter for valid E.164 format. The database approach is ideal for managing large contact lists, segmentation, opt-out tracking, and campaign history.
Next Steps and Production Deployment
Frequently Asked Questions
how to send bulk sms with next.js
Use Next.js API routes and the Plivo Node.js SDK. Create a secure API endpoint in your Next.js application that interacts with the Plivo API to send messages to multiple recipients simultaneously. This guide provides a detailed walkthrough for setting up this integration.
what is plivo used for in bulk messaging
Plivo is a cloud communications platform that provides the SMS API used to send bulk messages. The Plivo Node.js SDK simplifies integration with the Next.js application, allowing you to send messages efficiently without managing individual requests.
why use batching for sending bulk sms
Batching is essential because Plivo's API limits the number of recipients per request, often to around 50-100. The provided code example batches recipient numbers into chunks of 50, ensuring API limits are respected and avoiding potential issues.
when should I use a fallback sms provider with plivo
Consider a fallback provider for critical messages when high availability is essential. If Plivo experiences outages or consistent errors, your application could switch to the secondary provider. This guide doesn't include fallback implementation but explains the concept.
can I use prisma with plivo for contact lists
Yes, Prisma can be used for contact management, though it's optional. The provided guide demonstrates how to integrate Prisma with PostgreSQL to store and retrieve contact lists, making it easy to target specific groups for bulk messages.
how to set up plivo in a next.js project
First, install the Plivo Node.js SDK (`npm install plivo`). Then, create a `.env.local` file to securely store your Plivo Auth ID, Auth Token, and source number. Initialize the Plivo client in your code using these environment variables, making sure to never commit `.env.local` to version control.
what is the best way to handle plivo api errors
Implement robust error handling using try...catch blocks in your API route and service layer code. Return appropriate HTTP status codes (4xx or 5xx) with informative JSON error messages. Log errors with context using a structured logger like Pino for better debugging and monitoring.
why does plivo use the less than symbol for bulk sms
Plivo uses the less-than symbol (<) as a delimiter to separate multiple recipient numbers in the 'dst' parameter of a single API request. This allows you to send a single message to many recipients at once, efficiently utilizing the bulk sending capability.
when to retry sending a bulk sms with plivo
Retry sending when transient errors occur, such as network issues or temporary Plivo API problems. Implement retries with exponential backoff (increasing delays between attempts) to avoid overwhelming the API. However, be cautious of potential duplicate messages, especially if requests partially succeed.
how to secure my plivo bulk sms api endpoint
Use an internal API key and require the `Authorization: Bearer <key>` header in requests to your API endpoint. Store this key securely in environment variables (`.env.local`) and never expose it in client-side code or commit it to version control. This guide provides an example implementation.
what is e.164 format for phone numbers
E.164 is an international standard for phone number formatting, ensuring consistency and compatibility. It includes the '+' sign followed by the country code and national number, without any spaces or special characters (e.g., +12223334444). Validate phone numbers against this format to avoid errors.
how to get plivo auth id and auth token
Log in to your Plivo console at `https://console.plivo.com/`. Your Auth ID and Auth Token are displayed prominently on the main Dashboard page, usually in the top-right corner. Click the "eye" icon to reveal the Auth Token if it's hidden.
how to test bulk sms sending with plivo
Use `curl` to send test POST requests to your Next.js API endpoint. Provide a list of test phone numbers (in E.164 format) and a message in the JSON request body. Include the correct `Authorization` header with your internal API key. Check the responses for success or error messages.