code examples

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

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.

  1. Create Next.js App: Open your terminal and run:

    bash
    npx 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:

      bash
      cd plivo-bulk-sms-app
  2. Install Plivo SDK: Add the official Plivo Node.js helper library.

    bash
    npm install plivo
    # or
    yarn add plivo
    • Note: Use the plivo package (not the legacy plivo-node package which is deprecated). As of 2025, this is the actively maintained SDK.
  3. Install Prisma (Optional, for Contact Management): If you plan to manage contacts in a database, set up Prisma.

    bash
    npm 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 postgresql if you use a different database (e.g., mysql, sqlite).
    • Update the DATABASE_URL in the generated .env file with your actual database connection string.
  4. Set Environment Variables: Create a file named .env.local in 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., using openssl rand -base64 32 in your terminal).
      • DATABASE_URL: The connection string for your database if you use Prisma. Format depends on the database provider.
  5. Project Structure: Your src directory looks like this initially:

    text
    src/
    ├── 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 into lib/.

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.

  1. 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.
  2. 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.
  3. 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 chunkArray utility to divide recipients.
      • Formats the dst parameter correctly using the < delimiter as required by Plivo's bulk feature.
      • Iterates through batches and calls plivoClient.messages.create for 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 deliveryReportUrl parameter for status tracking (covered later).
      • Uses specific Plivo types (MessageCreateResponse) for better code safety where applicable.

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.

typescript
// 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_KEY via the Authorization: Bearer <key> header. Checks if the key is configured.
    • Validation: Checks the request body for required fields (message) and validates the format of numbers (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 sendBulkSms function in plivoService.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).
  • Test with curl:

    bash
    curl -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_KEY and 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.

  1. 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.local is listed in your .gitignore file.
  2. SDK Initialization: Covered in Section 2 (Core Functionality) in src/lib/plivo.ts. The plivo.Client initializes using the environment variables.

  3. API Call: The core interaction occurs in src/lib/plivoService.ts within the sendBulkSms function:

    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 your url (POST is recommended as it sends data in the body).
  4. 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.create fails 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.

  1. Error Handling Strategy:

    • API Route (route.ts): Use try…catch blocks to capture errors during request processing, validation, database access (if applicable), and calls to the sendBulkSms service. 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): Use try…catch within 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.
  2. Logging:

    • Current: Uses basic console.log and console.error. Sufficient for development.

    • Production: Integrate a structured logging library like pino or winston.

      bash
      npm install pino pino-pretty # pino-pretty for development formatting
      # or
      yarn add pino pino-pretty

      Create 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/error with logger.info/warn/error throughout your application (e.g., in route.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: info for routine operations (API calls, batch starts), warn for potential issues (retries, unexpected conditions), error for failures (API errors, exceptions).
      • Log Content: Include relevant context like request IDs (if available), batch numbers, error messages, and Plivo API responses/errors.
  3. 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 sendBulkSms batch loop to include retries with backoff. (This replaces the simple try/catch block 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-retry can 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 messageUuid helps track results but doesn't prevent duplicates if the create call is retried after initial success but before the response is received.

  4. Test Error Scenarios:

    • Invalid Credentials: Temporarily change PLIVO_AUTH_ID/PLIVO_AUTH_TOKEN in .env.local. Expect Plivo errors (likely HTTP 401) caught in plivoService.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 the numbers array. 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 curl request) or use tools like iptables (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).

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.