code examples

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

Vonage Bulk SMS with Node.js & Express: Complete Guide to Broadcasting Messages

Build a production-ready bulk SMS broadcasting system using Vonage API, Node.js, and Express. Learn rate limiting, 10DLC compliance, error handling, and best practices for sending mass SMS messages at scale.

Vonage Bulk SMS with Node.js & Express: Complete Broadcasting Guide

<!-- DEPTH: Introduction lacks context about scale - what is "large audiences"? How many recipients can this system handle? (Priority: Medium) --> <!-- GAP: Missing prerequisites - what should readers know about SMS APIs, rate limiting, or async programming before starting? (Type: Substantive) -->

This guide provides a complete walkthrough for building a robust bulk SMS broadcasting system using Node.js, Express, and the Vonage Communication APIs. Learn everything from project setup and core sending logic to error handling, security, performance optimization, and deployment, enabling you to reliably send messages to large audiences.

The final system features a simple REST API endpoint that accepts a list of recipients and a message, efficiently handles sending via Vonage, manages rate limits, logs results, and incorporates best practices for production environments.

Project Overview and Goals

What You're Building:

A Node.js application using the Express framework that exposes a secure API endpoint (/broadcast) to:

  1. Accept a list of phone numbers and a message payload.
  2. Iterate through the list and send the message to each recipient using the Vonage SMS API.
  3. Handle Vonage API rate limits gracefully (through client-side throttling or queuing).
  4. Provide structured logging for successful sends and errors.
  5. Offer a foundation for more advanced features like status tracking and scheduling.

Problem Solved:

<!-- DEPTH: Problem statement is too generic - needs real-world use cases and specific pain points this solves (Priority: Medium) -->

This application addresses the need to send SMS messages programmatically to multiple recipients simultaneously (broadcast) without manually sending each one. It provides a scalable and manageable way to handle bulk SMS communications for notifications, alerts, marketing campaigns, or other use cases.

Technologies Used:

  • Node.js: A JavaScript runtime built on Chrome's V8 engine, ideal for building scalable network applications. Node.js v22.x (Active LTS through April 2027) or v20.x (Maintenance LTS through mid-2026) recommended for production use.
  • Express.js: A minimal and flexible Node.js web application framework, providing a robust set of features for web and mobile applications.
  • Vonage SMS API: A powerful API for sending and receiving SMS messages globally. Use the @vonage/server-sdk (v3.24.1+ as of late 2024/early 2025) for Node.js.
  • dotenv: A zero-dependency module that loads environment variables from a .env file into process.env.
  • pino: A very low overhead Node.js logger for structured logging.
  • (Optional but Recommended) Prisma: A next-generation ORM (Object-Relational Mapping) for Node.js and TypeScript, useful for database interactions if storing broadcast history or recipient lists.
  • (Optional but Recommended) p-limit: For client-side request throttling.
  • (Optional but Recommended) express-rate-limit: Middleware for basic API endpoint rate limiting.
  • (Optional but Recommended) Helmet: Middleware for setting security-related HTTP headers.

System Architecture:

+-----------------+ +---------------------+ +-----------------+ +-----------------+ | API Client |----->| Node.js / Express |----->| Vonage SDK |----->| Vonage SMS API | | (e.g., curl, UI)| | (Broadcast API) | | (@vonage/server)| | | +-----------------+ +---------------------+ +-----------------+ +-----------------+ | Request | - Validate Input | Send SMS | (Recipients, Msg) | - Throttle/Queue Sends | Request | | - Call Vonage SDK | | | - Log Results (Structured) | |<---------------------| |<-----------------+ | Response |<-------------------------------------| Response (Status)| | (Success/Failure) +-----------------+ <!-- EXPAND: Architecture diagram could benefit from a table explaining each component's role and responsibilities (Type: Enhancement) -->

Prerequisites:

  • Node.js v22.x (Active LTS, recommended) or v20.x (Maintenance LTS) and npm installed.
  • A Vonage API account. Sign up here if you don't have one.
  • Your Vonage API Key and API Secret.
  • A Vonage virtual phone number or configured Alphanumeric Sender ID (availability depends on the destination country).
  • Basic familiarity with JavaScript, Node.js, and REST APIs.
  • (Optional) curl or a tool like Postman for testing the API.

Expected Outcome:

A functional Node.js Express application running locally, capable of accepting POST requests to /broadcast and sending SMS messages to the provided recipients via Vonage. The application includes structured logging, awareness of rate limiting, and basic security considerations.


1. Setting up the Project

Initialize your Node.js project and install the necessary dependencies.

<!-- GAP: Missing time estimate for setup phase - readers need to know how long this will take (Type: Substantive) -->
  1. Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.

    bash
    mkdir vonage-bulk-sms
    cd vonage-bulk-sms
  2. Initialize Node.js Project: This creates a package.json file to manage dependencies and project metadata.

    bash
    npm init -y
  3. Install Dependencies: Install Express, the Vonage SDK, dotenv, pino for logging, and optionally pino-pretty for development. Install the latest versions.

    bash
    npm install express @vonage/server-sdk dotenv pino
    npm install --save-dev pino-pretty # For development logging only
    • express: The web framework.
    • @vonage/server-sdk: The official Vonage Server SDK for Node.js (v3.24.1+ as of late 2024/early 2025).
    • dotenv: Loads environment variables from a .env file.
    • pino: Fast, structured JSON logger.
    • pino-pretty: (Dev dependency) Formats Pino logs for readability during development.
<!-- DEPTH: Package dependency explanations are too brief - why choose Pino over Winston? What are trade-offs? (Priority: Low) -->
  1. Enable ES Modules (Optional but Recommended): To use modern import/export syntax, open your package.json file and add the following line:

    json
    // package.json
    {
      "name": "vonage-bulk-sms",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "type": "module", // <-- Add this line
      "scripts": {
        "start": "node index.js",
        "dev": "node index.js | pino-pretty", // Example dev script using pino-pretty
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "@vonage/server-sdk": "^3.24.1", // Latest stable as of late 2024/early 2025
        "dotenv": "^16.0.0",           // Example version, use actual installed version
        "express": "^4.0.0",          // Example version, use actual installed version
        "pino": "^8.0.0"              // Example version, use actual installed version
      },
      "devDependencies": {
        "pino-pretty": "^9.0.0"       // Example version, use actual installed version
      }
    }
  2. Create Core Files: Create the main application file, logger configuration, service files, and environment file.

    bash
    touch index.js logger.js vonageClient.js smsService.js .env
  3. Configure Environment Variables (.env): Open the .env file and add your Vonage credentials and sender ID. Never commit this file to version control.

    dotenv
    # .env
    
    # Vonage API Credentials
    # Found in your Vonage Dashboard: https://dashboard.nexmo.com/getting-started/api-accounts
    VONAGE_API_KEY=YOUR_API_KEY
    VONAGE_API_SECRET=YOUR_API_SECRET
    
    # Vonage Sender ID (Virtual Number or Alphanumeric Sender ID)
    # Purchase numbers: https://dashboard.nexmo.com/numbers/buy
    # Note: Alphanumeric Sender ID support varies by country and requires registration.
    # For US/Canada, you MUST use a Vonage number registered for A2P 10DLC.
    # As of October 2024, all brands require Brand Authentication+ (2FA email verification).
    # As of January 2024, carriers require opt-out instructions in campaign sample messages.
    VONAGE_SENDER_ID=YOUR_VONAGE_NUMBER_OR_SENDER_ID
    
    # Application Port
    PORT=3000
    
    # Logging Level (e.g., "info", "debug", "warn", "error")
    LOG_LEVEL=info
    
    # (Optional) API Key for securing the broadcast endpoint (See Section 7)
    # INTERNAL_API_KEY=your-secret-api-key-here
    • VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_SENDER_ID: Your Vonage details. For sending to US numbers, VONAGE_SENDER_ID must be a 10DLC-registered Vonage number with verified brand and campaign. See Section 8 for 2024 compliance updates.
    • PORT: Application port.
    • LOG_LEVEL: Controls logging verbosity.
    • INTERNAL_API_KEY: (Optional) Used for basic API security later.
<!-- GAP: Missing security warning about .gitignore setup - critical to prevent credential leaks (Type: Critical) -->
  1. Configure Logger (logger.js): Set up the shared Pino logger instance.

    javascript
    // logger.js
    import pino from 'pino';
    
    const logger = pino({
      level: process.env.LOG_LEVEL || 'info',
      // Use pino-pretty for development, structured JSON for production
      transport: process.env.NODE_ENV !== 'production'
        ? { target: 'pino-pretty', options: { colorize: true } }
        : undefined, // In production, log JSON to stdout
    });
    
    export default logger;
  2. Create Basic Express Server (index.js): Set up the initial Express application structure using the logger.

    javascript
    // index.js
    import express from 'express';
    import 'dotenv/config'; // Load .env variables into process.env
    import logger from './logger.js'; // Import the configured logger
    
    const app = express();
    const port = process.env.PORT || 3000;
    
    // Middleware to parse JSON bodies
    app.use(express.json());
    // Middleware to parse URL-encoded bodies
    app.use(express.urlencoded({ extended: true }));
    
    // Simple health check endpoint
    app.get('/health', (req, res) => {
      res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() });
    });
    
    // Placeholder for our broadcast endpoint
    app.post('/broadcast', (req, res) => {
      // We will implement this in the next steps
      logger.warn('Broadcast endpoint hit but not implemented yet.');
      res.status(501).json({ message: 'Broadcast endpoint not implemented yet.' });
    });
    
    // Global error handler (using logger)
    app.use((err, req, res, next) => {
      logger.error({ err: err, stack: err.stack }, 'An unexpected error occurred');
      res.status(500).json({ message: 'Internal Server Error' });
    });
    
    
    app.listen(port, () => {
      logger.info(`Server running on http://localhost:${port}`);
      // Check if Vonage credentials are loaded (basic check)
      if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_SENDER_ID) {
        logger.warn('Vonage API credentials or Sender ID are missing in .env file. SMS sending will fail.');
      }
    });
    
    export default app; // Export app for potential testing
    • We import and use our logger.
    • A dev script is added to package.json to pipe output through pino-pretty.
  3. Run the Application: Start the server using the dev script for readable logs.

    bash
    npm run dev

    You should see "Server running on http://localhost:3000". Test the health check: curl http://localhost:3000/health.

<!-- GAP: Missing troubleshooting subsection - what if server fails to start? Common errors? (Type: Substantive) -->

2. Implementing Core Functionality: Sending SMS

Implement the logic to send SMS messages using the Vonage SDK and your logger.

  1. Initialize Vonage SDK (vonageClient.js):

    javascript
    // vonageClient.js
    import { Vonage } from '@vonage/server-sdk';
    import 'dotenv/config';
    import logger from './logger.js';
    
    // Basic validation for environment variables
    if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET) {
      logger.fatal('Vonage API Key or Secret is not defined in .env file. Application cannot start.');
      process.exit(1); // Exit if essential credentials are missing
    }
    
    const vonage = new Vonage({
      apiKey: process.env.VONAGE_API_KEY,
      apiSecret: process.env.VONAGE_API_SECRET,
      // Optional: Specify custom user agent for easier debugging/tracking
      userAgent: "vonage-bulk-sms-guide/1.0.0"
    });
    
    logger.info('Vonage SDK initialized.');
    
    export default vonage;
    • Use the logger for initialization messages and fatal errors.
<!-- DEPTH: No explanation of userAgent purpose or how it helps in production debugging (Priority: Low) -->
  1. Create the Sending Function (smsService.js):

    javascript
    // smsService.js
    import vonage from './vonageClient.js'; // Import the initialized client
    import logger from './logger.js'; // Import the logger
    
    const sender = process.env.VONAGE_SENDER_ID;
    
    /**
     * Sends a single SMS message using the Vonage API.
     * @param {string} recipient - The recipient's phone number (E.164 format recommended).
     * @param {string} message - The text message content.
     * @returns {Promise<object>} - A promise that resolves with a structured result object.
     */
    export async function sendSingleSms(recipient, message) {
      if (!sender) {
        logger.error('VONAGE_SENDER_ID is not defined in .env file.');
        // Throwing an error here might stop a whole batch; returning failure is often better.
        return { success: false, recipient: recipient, error: 'Server configuration error: Missing Sender ID', status: 'config_error' };
      }
      if (!recipient || !message) {
        logger.warn({ recipient: recipient }, 'Attempted to send SMS with missing recipient or message.');
        return { success: false, recipient: recipient || 'unknown', error: 'Recipient and message parameters are required.', status: 'param_error' };
      }
    
      const logPayload = { recipient: recipient, sender: sender };
      logger.info(logPayload, `Attempting to send SMS to ${recipient}...`);
    
      try {
        const responseData = await vonage.sms.send({
          to: recipient,
          from: sender,
          text: message,
          // Optional: Specify 'type: unicode' if your message contains emojis or non-GSM characters
          // type: 'unicode'
        });
    
        // Check the response status for the first (and usually only) message part
        if (responseData.messages[0].status === '0') {
          logger.info({ ...logPayload, msgId: responseData.messages[0]['message-id'] }, `Message sent successfully to ${recipient}.`);
          return { success: true, recipient: recipient, data: responseData.messages[0] };
        } else {
          const errorText = responseData.messages[0]['error-text'];
          const statusCode = responseData.messages[0].status;
          logger.error({ ...logPayload, vonageStatus: statusCode, vonageError: errorText, msgId: responseData.messages[0]['message-id'] }, `Message failed to send to ${recipient}.`);
          // We still resolve, but indicate failure in the returned object
          return { success: false, recipient: recipient, error: errorText, status: statusCode };
        }
      } catch (error) {
        logger.error({ ...logPayload, error: error }, `Failed to send SMS to ${recipient} due to API/SDK error.`);
        // Propagate the error for higher-level handling, but include recipient info
        return { success: false, recipient: recipient, error: error.message || 'Unknown API error', status: 'api_error' };
      }
    }
    • Replaced all console.* with logger.* calls, providing context objects.
    • Handles missing sender by returning a failure object instead of throwing, which is often better in batch processes.
    • Includes more contextual info (like msgId, vonageStatus) in logs.
<!-- GAP: Missing phone number validation function - E.164 format validation is critical but not implemented (Type: Critical) --> <!-- DEPTH: No explanation of Vonage response structure or what different status codes mean (Priority: High) -->

3. Building the API Layer

Implement the /broadcast endpoint using your service and Promise.allSettled.

  1. Import Service and Implement Endpoint (index.js): Update index.js to handle the /broadcast route.

    javascript
    // index.js
    import express from 'express';
    import 'dotenv/config';
    import logger from './logger.js';
    import { sendSingleSms } from './smsService.js'; // Import the sending function
    // import { requireApiKey } from './apiKeyAuth.js'; // Uncomment when adding API key auth
    // import rateLimit from 'express-rate-limit'; // Uncomment when adding rate limiting
    
    const app = express();
    const port = process.env.PORT || 3000;
    
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));
    
    // --- Health Check ---
    app.get('/health', (req, res) => {
      res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() });
    });
    
    // --- (Optional) API Endpoint Rate Limiter ---
    /* // Uncomment to enable
    const broadcastLimiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per windowMs
      message: 'Too many broadcast requests created from this IP, please try again after 15 minutes',
      standardHeaders: true, legacyHeaders: false,
      handler: (req, res, next, options) => {
          logger.warn({ ip: req.ip }, `Rate limit exceeded for ${req.ip}`);
          res.status(options.statusCode).send(options.message);
      }
    });
    */
    
    // --- Broadcast Endpoint Implementation ---
    app.post('/broadcast',
      // broadcastLimiter, // Uncomment to apply rate limiting
      // requireApiKey, // Uncomment to apply API key authentication
      async (req, res, next) => {
      const { recipients, message } = req.body;
    
      // --- 1. Input Validation ---
      if (!Array.isArray(recipients) || recipients.length === 0) {
        logger.warn({ body: req.body }, 'Invalid broadcast request: recipients missing or not an array.');
        return res.status(400).json({ message: 'Invalid input: "recipients" must be a non-empty array.' });
      }
      if (typeof message !== 'string' || message.trim() === '') {
        logger.warn({ body: req.body }, 'Invalid broadcast request: message missing or empty.');
        return res.status(400).json({ message: 'Invalid input: "message" must be a non-empty string.' });
      }
      // Add more validation (e.g., phone number format, max recipients) here if needed
    
      const uniqueRecipients = [...new Set(recipients.map(r => String(r).trim()))]; // Deduplicate and trim
      const recipientCount = uniqueRecipients.length;
      logger.info({ recipientCount: recipientCount }, `Received broadcast request for ${recipientCount} unique recipients.`);
    
      try {
        // --- 2. Process Sends Concurrently (with awareness of limits) ---
        // NOTE: This basic Promise.allSettled approach sends all requests concurrently.
        // For production bulk sending (more than ~20 – 30 recipients), you MUST implement
        // client-side throttling (see Section 5 using p-limit) or preferably a
        // job queue system (see Section 9) to avoid hitting Vonage rate limits.
        const sendPromises = uniqueRecipients.map(recipient =>
          sendSingleSms(recipient, message)
        );
    
        // Wait for all send attempts to complete (either success or failure)
        const results = await Promise.allSettled(sendPromises);
    
        // --- 3. Aggregate Results ---
        let successfulSends = 0;
        let failedSends = 0;
        const detailedResults = results.map((result, index) => {
          const recipient = uniqueRecipients[index]; // Get recipient corresponding to this result
          if (result.status === 'fulfilled') {
             // Check the success flag returned by sendSingleSms
            if(result.value.success) {
              successfulSends++;
              return { recipient: recipient, status: 'Sent', messageId: result.value.data['message-id'] };
            } else {
              failedSends++;
              return { recipient: recipient, status: 'Failed', error: result.value.error, vonage_status: result.value.status };
            }
          } else {
            // Promise itself rejected (e.g., unexpected error within sendSingleSms before return)
            failedSends++;
            logger.error({ recipient: recipient, reason: result.reason }, 'Send promise rejected unexpectedly.');
            return { recipient: recipient, status: 'Failed', error: result.reason?.message || 'Promise rejected' };
          }
        });
    
        logger.info({ recipientCount, successfulSends, failedSends }, `Broadcast finished processing.`);
    
        // --- 4. Send Response ---
        res.status(200).json({
          message: `Broadcast processed for ${recipientCount} unique recipients.`,
          successful_sends: successfulSends,
          failed_sends: failedSends,
          details: detailedResults,
        });
    
      } catch (error) {
        // Pass unexpected errors (outside promise handling) to the global error handler
        logger.error({ error: error }, 'Unhandled error during broadcast processing.');
        next(error);
      }
    });
    // --- End Broadcast Endpoint ---
    
    
    // Global error handler
    app.use((err, req, res, next) => {
      // Log the error including stack trace
      logger.error({ err: { message: err.message, stack: err.stack }, url: req.originalUrl, method: req.method }, 'An unexpected error occurred handling a request.');
      // Avoid sending stack trace to client in production
      const responseMessage = process.env.NODE_ENV === 'production' ? 'Internal Server Error' : err.message;
      res.status(err.status || 500).json({ message: responseMessage });
    });
    
    
    app.listen(port, () => {
      logger.info(`Server running on http://localhost:${port}`);
      if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_SENDER_ID) {
        logger.warn('Vonage API credentials or Sender ID are missing in .env file. SMS sending will fail.');
      }
       if (!process.env.INTERNAL_API_KEY && process.env.NODE_ENV !== 'production') {
         logger.warn('INTERNAL_API_KEY is not set. The /broadcast endpoint may be unsecured if API key middleware is enabled.');
       }
    });
    
    export default app; // Export for testing
    • Integrate sendSingleSms and logger.
    • Use Promise.allSettled to handle concurrent sends.
    • Include a clear NOTE warning about the need for throttling/queuing in production for this basic concurrent approach.
    • Aggregate results and provide a detailed response.
    • Include placeholders for adding API key auth and rate limiting middleware later.
    • Improved global error handler logging.
<!-- GAP: Missing validation for maximum recipients limit - should prevent DOS via huge recipient lists (Type: Critical) --> <!-- GAP: Missing message length validation - SMS has character limits that aren't checked (Type: Substantive) -->
  1. Test the Endpoint: Restart your server (npm run dev). Use curl or Postman:

    bash
    # Replace YOUR_PHONE_NUMBER_1/2 with actual E.164 formatted numbers
    # Ensure these numbers are registered as test numbers in your Vonage dashboard if using a trial account
    curl -X POST http://localhost:3000/broadcast \
       -H "Content-Type: application/json" \
       -d '{
             "recipients": ["YOUR_PHONE_NUMBER_1", "YOUR_PHONE_NUMBER_2", "1555INVALID"],
             "message": "Hello from Vonage Bulk Sender! (Test v2)"
           }'

    Check your terminal for structured logs (formatted by pino-pretty) and the JSON response, which should detail success/failure for each recipient.

<!-- DEPTH: Testing section lacks expected output examples - show what successful/failed responses look like (Priority: Medium) -->

4. Integrating with Vonage (Setup Recap & Details)

This section consolidates the Vonage-specific setup steps.

  1. Sign Up/Log In: Access the Vonage API Dashboard.
  2. Get API Key and Secret: Find these in the "API settings" section and add them to your .env file (VONAGE_API_KEY, VONAGE_API_SECRET). Keep the secret secure.
  3. Obtain a Sender ID (Vonage Number):
    • Buy an SMS-capable number in the "Numbers" section of the dashboard.
    • Add the number (E.164 format) to .env as VONAGE_SENDER_ID.
    • A2P 10DLC (USA): CRITICAL. If sending to US numbers using a US long code, you must register your Brand and Campaign via the Vonage dashboard ("Brands and Campaigns") and link your number(s). Failure leads to blocking or severe throughput restrictions. See Section 8 for detailed 2024 compliance requirements.
    • Alphanumeric Sender ID: Check Vonage documentation for country support and registration requirements if needed. Generally not allowed for A2P traffic to US/Canada.
  4. Test Numbers (Trial Accounts): Verify recipient numbers in the dashboard if using a trial account.
  5. Environment Variables Review: Ensure .env has VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_SENDER_ID, PORT, LOG_LEVEL.
<!-- GAP: Missing cost information - readers need to understand SMS pricing structure (Type: Substantive) --> <!-- GAP: Missing trial account limitations - how many free messages, what restrictions exist? (Type: Substantive) -->

5. Error Handling, Logging, and Retry Mechanisms

Production systems need robust handling.

  1. Consistent Error Handling: The sendSingleSms function returns structured objects ({ success: boolean, ... }) even on failure, allowing the broadcast endpoint to aggregate results properly. The global error handler catches unexpected exceptions.

  2. Structured Logging: Integrate pino (see logger.js).

    • Logs are in JSON format (good for log aggregators).
    • pino-pretty used for readable development logs (npm run dev).
    • Log levels (info, warn, error, fatal) are used appropriately. Contextual data is included in log objects.
<!-- EXPAND: Could benefit from a table showing which log level to use for different scenarios (Type: Enhancement) -->
  1. Rate Limiting & Throttling (CRUCIAL for Bulk):
    • Vonage Limits: Per Vonage API Support documentation, all Vonage API keys have a default limit of 30 API requests per second for outbound SMS (up to 2,592,000 SMS per day). Higher throughput can be arranged through your account manager for enterprise use cases.

    • US 10DLC Throughput: US long code numbers have additional per-number throughput limits based on campaign type (typically 1 – 60 messages per second depending on brand trust score and campaign classification). See 10DLC Throughput Limits documentation.

    • Client-Side Throttling: The Promise.allSettled in Section 3 is insufficient for production bulk sending. Throttle requests using proper concurrency control.

      • Recommended Library: p-limit: Control concurrency.
      bash
      npm install p-limit

      Example using p-limit in the /broadcast handler (Conceptual Integration):

      javascript
      // In index.js /broadcast handler
      import pLimit from 'p-limit';
      // ... other imports like logger, sendSingleSms ...
      
      // Define the concurrency limit.
      // START CONSERVATIVELY! e.g., 5 – 10.
      // For standard accounts, keep below 30 req/sec API limit.
      // For US 10DLC, also consider per-number throughput limits (1 – 60 msg/sec).
      // Adjust based on testing and observed error rates (429 errors, status code 1).
      const limit = pLimit(10); // Limit to 10 concurrent sendSingleSms calls
      
      // ... inside app.post('/broadcast', async (req, res, next) => { ... try block
      
      logger.info({ concurrency: 10 }, 'Processing sends with concurrency limit.'); // Log the limit used
      
      // Wrap the sendSingleSms call with the limiter inside the map
      const sendPromises = uniqueRecipients.map(recipient =>
          limit(() => sendSingleSms(recipient, message))
      );
      
      // Use Promise.allSettled here. It works well with p-limit.
      // It waits for all limited tasks to complete, regardless of individual success/failure.
      const results = await Promise.allSettled(sendPromises);
      
      // Process results (aggregation logic remains the same as Section 3)
      // ... rest of endpoint
      • Job Queue (Best for High Volume): See Section 9. Decouples API response from sending, handles rate limiting in background workers.
<!-- DEPTH: p-limit example needs more explanation of how it actually prevents rate limit errors (Priority: Medium) --> <!-- GAP: Missing monitoring section - how to detect when rate limits are being hit in production? (Type: Substantive) -->
  1. Retry Mechanisms:
    • Selective Retries: Only retry on transient errors (network issues, temporary Vonage 5xx errors, rate limit errors status: 1 or HTTP 429). Avoid retrying permanent failures (invalid number status: 3, auth errors status: 4, unroutable status: 9).
    • Use Libraries: async-retry can help implement retries with exponential backoff.
    • Queue-Based Retries: Job queues (Section 9) usually have robust built-in retry features.
<!-- GAP: Missing concrete retry implementation example - readers need working code not just concepts (Type: Substantive) -->

6. Creating a Database Schema and Data Layer (Optional)

For tracking history and detailed status, use a database.

<!-- GAP: Section title says "Optional" but for production bulk SMS this is actually mandatory for audit trails (Type: Critical) -->
  1. Technology Choice: PostgreSQL/MySQL with Prisma ORM recommended.

  2. Schema Design (Example using Prisma):

    prisma
    // prisma/schema.prisma
    
    datasource db {
      provider = "postgresql" // or "mysql", "sqlite"
      url      = env("DATABASE_URL")
    }
    
    generator client {
      provider = "prisma-client-js"
    }
    
    model Broadcast {
      id              String    @id @default(cuid())
      message         String
      createdAt       DateTime  @default(now())
      status          String    @default("PENDING") // PENDING, PROCESSING, COMPLETED, FAILED
      totalRecipients Int
      submittedCount  Int       @default(0) // Count submitted to Vonage
      successCount    Int       @default(0) // Count confirmed success by Vonage API
      failedCount     Int       @default(0) // Count confirmed failure by Vonage API or SDK error
      deliveredCount  Int       @default(0) // Count confirmed delivered via DLR
      dlrFailedCount  Int       @default(0) // Count confirmed failed/expired via DLR
      messages        Message[] // Relation to individual message statuses
    }
    
    model Message {
      id            String     @id @default(cuid())
      broadcast     Broadcast  @relation(fields: [broadcastId], references: [id], onDelete: Cascade)
      broadcastId   String     @db.VarChar(255)
      recipient     String     @db.VarChar(20) // Store in E.164
      status        String     // PENDING, SUBMITTED, FAILED_API, FAILED_DLR, DELIVERED, EXPIRED
      vonageMsgId   String?    @unique @db.VarChar(50) // Store the message ID from Vonage
      vonageStatus  String?    @db.VarChar(10) // Store Vonage status code (e.g., '0', '3', '4')
      errorCode     String?    // Store DLR error code if failed
      errorText     String?    // Store Vonage error text or DLR description
      submittedAt   DateTime   @default(now())
      dlrReceivedAt DateTime?  // Timestamp when DLR was processed
      updatedAt     DateTime   @updatedAt
    
      @@index([broadcastId])
      @@index([status])
      @@index([recipient])
    }
    • Detailed status tracking for broadcasts and individual messages. Includes fields for DLR updates.
<!-- DEPTH: Schema design lacks explanation of why specific indexes are chosen and performance implications (Priority: Low) --> <!-- GAP: Missing data retention policy guidance - how long to keep broadcast history? Legal requirements? (Type: Substantive) -->
  1. Setup Prisma: Follow standard Prisma setup (npm install prisma @prisma/client --save-dev, npx prisma init, configure .env, add schema, npx prisma migrate dev, npx prisma generate).

  2. Data Access Layer (Conceptual): Create services using Prisma Client.

    javascript
    // broadcastDbService.js (Conceptual - Requires Prisma setup)
    import { PrismaClient } from '@prisma/client';
    const prisma = new PrismaClient(); // Assumes Prisma client is generated
    
    export async function createBroadcastRecord(message, recipients) {
      const uniqueRecipients = [...new Set(recipients.map(r => String(r).trim()))];
      logger.info(`Creating broadcast record for ${uniqueRecipients.length} recipients.`);
      return prisma.broadcast.create({
        data: {
          message: message,
          totalRecipients: uniqueRecipients.length,
          status: 'PROCESSING', // Or 'QUEUED' if using a job queue
          messages: {
            create: uniqueRecipients.map(r => ({ recipient: r, status: 'PENDING' }))
          }
        },
        include: { messages: true } // Include messages for immediate processing
      });
    }
    
    export async function updateMessageStatusAfterSend(messageDbId, sendResult) {
      let updateData = {
         submittedAt: new Date(), // Mark submission time
         updatedAt: new Date()
      };
      if (sendResult.success) {
        updateData.status = 'SUBMITTED'; // Submitted to Vonage, awaiting DLR
        updateData.vonageMsgId = sendResult.data['message-id'];
        updateData.vonageStatus = sendResult.data.status; // Should be '0'
      } else {
        updateData.status = 'FAILED_API'; // Failed during API call or rejected by Vonage
        updateData.vonageStatus = sendResult.status; // Vonage status code (e.g., '3', '4') or custom error status
        updateData.errorText = sendResult.error;
      }
       logger.debug({ messageDbId, status: updateData.status, vonageMsgId: updateData.vonageMsgId }, 'Updating message status after send attempt.');
      return prisma.message.update({ where: { id: messageDbId }, data: updateData });
    }
    
    // Function to update broadcast counts based on message outcomes (call after batch finishes)
    export async function updateBroadcastSummary(broadcastId) {
        // Complex logic: Query message statuses for the broadcastId, calculate counts,
        // update Broadcast record (submittedCount, successCount, failedCount),
        // potentially update overall Broadcast status (e.g., to COMPLETED).
        // Consider using Prisma aggregate functions.
        logger.info({ broadcastId }, 'Updating broadcast summary counts.');
        // ... implementation omitted for brevity ...
    }
    
    // Function needed for DLR processing (Section 10)
    export async function updateMessageStatusFromDlr(vonageMsgId, dlrStatus, errorCode) {
        // Update Message record based on delivery receipt
        // Map DLR status to internal status (DELIVERED, FAILED_DLR, EXPIRED, etc.)
        logger.info({ vonageMsgId, dlrStatus, errorCode }, 'Processing DLR update.');
        // ... implementation to find message by vonageMsgId and update status ...
    }
<!-- GAP: Missing implementation for updateBroadcastSummary - marked as omitted but this is crucial functionality (Type: Critical) --> <!-- GAP: Missing connection pooling configuration - Prisma clients need proper lifecycle management (Type: Substantive) -->

8. Compliance and Messaging Regulations (USA A2P 10DLC – 2024 Updates)

Critical for US SMS Traffic: If you're sending SMS to US phone numbers using a US 10-digit long code, you must comply with A2P 10DLC regulations. Non-compliance results in blocked messages or severe throughput restrictions.

What is A2P 10DLC?

A2P (Application-to-Person) 10DLC is the industry standard for business messaging over 10-digit local phone numbers in the United States. All US long code SMS traffic requires Brand and Campaign registration with mobile carriers via The Campaign Registry (TCR).

<!-- EXPAND: Could benefit from a flowchart showing the compliance decision tree (Type: Enhancement) -->

2024 Compliance Requirements

Per Vonage's official 10DLC documentation, the following requirements are mandatory:

  1. Brand Registration:

    • All brands sending A2P messages must be registered and traced to a single legal entity
    • Brand Authentication+ (Since October 2024): All new and existing public profit brands require 2FA email verification for brand verification
    • Brands must achieve "verified" or "vetted verified" status to register campaigns
    • Source: 10DLC Brand Registration Guide
  2. Campaign Registration:

    • Each messaging use case requires a separate campaign registration
    • Campaign Update Reviews (Since October 2024): Updating existing campaigns triggers a new compliance review
    • Campaigns must include detailed call-to-action (CTA) documentation
    • Source: 10DLC Campaign Registration Guide
  3. Call-to-Action Requirements (Since January 2024):

    • Brands must prove compliant CTA for all consent mechanisms
    • Sample messages must include opt-out instructions (e.g., "Reply STOP to unsubscribe")
    • Documentation must outline exact CTA language including legal mentions and required statements
    • Hosted documentation links must be provided during campaign registration
    • Source: 10DLC Update – New Campaign Requirements (January 2024)
  4. Throughput Limitations:

    • Message throughput varies by brand trust score and campaign type
    • Standard campaigns: 1 – 6 messages per second per long code
    • High-trust verified brands: Up to 60 messages per second
    • Source: 10DLC Throughput Limits
  5. Ongoing Compliance:

    • Regular compliance audits by carriers
    • Failure to maintain compliance results in campaign suspension or message blocking
    • Keep CTA documentation and consent records updated
<!-- GAP: Missing cost information for 10DLC registration - Brand and Campaign fees can be significant (Type: Substantive) --> <!-- GAP: Missing timeline expectations beyond "1-5 business days" - need worst-case scenarios (Type: Substantive) -->

Registration Process

  1. Navigate to Vonage Dashboard → "Brands and Campaigns"
  2. Complete Brand registration (include EIN/business registration)
  3. Wait for Brand verification (can take 1 – 5 business days)
  4. Register Campaign with detailed use case description
  5. Provide CTA documentation (hosted links to consent flows)
  6. Include opt-out instructions in sample messages
  7. Link registered Vonage numbers to approved campaign
  8. Wait for carrier approval (typically 1 – 2 weeks)
<!-- DEPTH: Registration process lacks troubleshooting - what if registration is rejected? Common reasons? (Priority: High) -->

Non-US Traffic

For non-US destinations, 10DLC registration is not required, but other regulations may apply:

  • Canada: Registration requirements for local long codes
  • Europe: GDPR compliance for data handling
  • Global: Local telecommunications regulations and sender ID restrictions

Important: Always consult Vonage's compliance documentation for the most current requirements.

<!-- GAP: Missing section on international regulations - GDPR, TCPA, CASL are mentioned but not explained (Type: Substantive) -->

Frequently Asked Questions

<!-- EXPAND: FAQ section could benefit from categorization (Getting Started, Compliance, Technical, Troubleshooting) (Type: Enhancement) -->

What is the Vonage SMS API rate limit for bulk sending?

All Vonage API keys have a default limit of 30 API requests per second for outbound SMS (up to 2,592,000 SMS per day). For US 10DLC numbers, additional per-number throughput limits apply based on campaign type: standard campaigns support 1 – 6 messages per second per long code, while high-trust verified brands can send up to 60 messages per second. Use client-side throttling with libraries like p-limit to stay within limits.

How do I handle Vonage API rate limit errors in Node.js?

Implement client-side throttling using p-limit to control concurrency (start with 5 – 10 concurrent requests). Monitor for rate limit errors (Vonage status code 1 or HTTP 429) and implement retry logic with exponential backoff using libraries like async-retry. For production bulk sending, use a job queue system (Bull, BullMQ) to decouple API responses from message sending and handle rate limiting in background workers.

What is A2P 10DLC and why is it required for US SMS?

A2P (Application-to-Person) 10DLC is the mandatory industry standard for business messaging over 10-digit local phone numbers in the United States. All US long code SMS traffic requires Brand and Campaign registration with mobile carriers via The Campaign Registry (TCR). As of October 2024, brands require Brand Authentication+ (2FA email verification), and as of January 2024, sample messages must include opt-out instructions. Non-compliance results in blocked messages or severe throughput restrictions.

How do I register for Vonage 10DLC compliance?

Navigate to Vonage Dashboard → "Brands and Campaigns", complete Brand registration (include EIN/business registration), wait for Brand verification (1 – 5 business days), register Campaign with detailed use case description, provide CTA documentation with hosted links to consent flows, include opt-out instructions in sample messages, link registered Vonage numbers to approved campaign, and wait for carrier approval (typically 1 – 2 weeks). All new brands since October 2024 require 2FA email verification.

What Node.js version should I use for Vonage SMS applications?

Use Node.js v22.x (Active LTS through April 2027) or v20.x (Maintenance LTS through mid-2026) for production applications. The Vonage Server SDK (@vonage/server-sdk) v3.24.1+ (as of late 2024/early 2025) is compatible with both versions. Avoid using odd-numbered Node.js versions (e.g., v21, v23) as they are Current releases without long-term support.

How do I send bulk SMS without hitting rate limits?

Use p-limit to control concurrency and keep requests below 30 per second. For US 10DLC numbers, also respect per-number throughput limits (1 – 60 msg/sec based on campaign type). For high-volume bulk sending (100+ recipients), implement a job queue system using Bull or BullMQ to process sends in background workers with proper throttling. Monitor Vonage response status codes and implement selective retry logic only for transient errors (network issues, temporary 5xx errors).

What Vonage error codes should I not retry?

Do not retry permanent failures: invalid number (status: 3), authentication errors (status: 4), unroutable destination (status: 9), and configuration errors. Only retry transient errors: rate limits (status: 1, HTTP 429), network issues, and temporary Vonage server errors (5xx status codes). Implement exponential backoff for retries and set a maximum retry limit (typically 3 – 5 attempts) to avoid infinite loops.

How do I track message delivery status with Vonage?

Configure a webhook endpoint in your Vonage Dashboard to receive Delivery Receipt (DLR) callbacks. Vonage sends POST requests to your webhook URL with delivery status updates including delivered, failed, expired, and rejected statuses. Store the Vonage message ID (message-id) from the initial send response to correlate DLRs with database records. Update message status in your database when DLRs arrive to track final delivery outcomes for reporting and compliance.

<!-- GAP: Missing FAQ about webhook security - how to verify DLR callbacks are actually from Vonage (Type: Critical) -->

Can I send Unicode characters and emojis via Vonage SMS?

Yes. Include type: 'unicode' in the vonage.sms.send() options to support Unicode characters, emojis, and non-GSM alphabets. Unicode messages use UCS-2 encoding and have a reduced character limit (70 characters per SMS segment instead of 160 for GSM-7). Vonage automatically handles message concatenation for longer Unicode messages. Note that Unicode messages cost the same as standard SMS but consume more segments due to the lower character limit.

How do I secure the Vonage bulk broadcast API endpoint?

Implement multiple security layers: (1) Use API key authentication with a strong secret stored in environment variables (INTERNAL_API_KEY), (2) Add rate limiting with express-rate-limit to prevent abuse (e.g., 100 requests per 15 minutes per IP), (3) Use Helmet middleware to set security-related HTTP headers, (4) Validate and sanitize all input data using libraries like Zod, (5) Implement request signing or JWT tokens for authenticated API access, (6) Use HTTPS in production to encrypt data in transit, and (7) Log all API requests with correlation IDs for security auditing.

<!-- GAP: Missing actual implementation example for API key authentication - referenced but never implemented (Type: Substantive) -->

What database schema should I use for bulk SMS tracking?

Create two main models: Broadcast (tracks batch metadata: message content, total recipients, status, counts for submitted/successful/failed/delivered) and Message (tracks individual message details: recipient, status, Vonage message ID, Vonage status code, error text, submission timestamp, DLR timestamp). Use indexes on broadcastId, status, recipient, and vonageMsgId for query performance. Store phone numbers in E.164 format and include fields for DLR updates to track final delivery status from Vonage webhooks.

Frequently Asked Questions

How to send bulk SMS messages with Node.js?

Use Node.js with Express.js and the Vonage SMS API to build a system that can send messages to large groups. The Vonage API allows you to send many SMS messages at once via API requests, and Node.js provides the server-side environment to manage the process efficiently.

What is the Vonage SMS API used for?

The Vonage SMS API is a service that enables sending and receiving SMS messages programmatically to numbers worldwide. It's ideal for sending bulk SMS messages, handling replies, and managing global communications within an application.

Why does bulk SMS messaging require throttling?

Throttling or queuing in bulk SMS is crucial to respect Vonage API rate limits (around 30 requests per second) and individual carrier limits (often 1 SMS per second per number). Exceeding these leads to failed sends (429 errors) and potential account restrictions.

When should I use a job queue for SMS broadcasts?

A job queue (like Redis Queue) is recommended for high-volume bulk SMS sending to manage rate limits and handle retries reliably. It decouples the API request from the sending process, enabling asynchronous processing without blocking the main application thread.

Can I track the status of my sent SMS messages?

Yes, using a database (like Postgres with Prisma) to store message details and implementing DLR (Delivery Receipt) handling, you can track the status (sent, delivered, failed) of each individual SMS message within a broadcast. This provides valuable insights into message delivery outcomes.

How to set up Vonage for bulk SMS in Node.js?

Get your API key and secret from the Vonage dashboard, purchase a Vonage number or alphanumeric Sender ID (with A2P 10DLC registration if sending to US numbers), add these credentials to your project's .env file, and initialize the Vonage SDK in your Node.js application. For US numbers, remember that the Sender ID usually needs to be a Vonage number registered for A2P 10DLC.

What is A2P 10DLC for Vonage SMS?

A2P 10DLC (Application-to-Person 10-Digit Long Code) is a system in the US for registering businesses and campaigns that send application-to-person SMS messages using 10-digit long code numbers. It's required to avoid message blocking or filtering, especially when sending to US recipients.

How to handle Vonage SMS API rate limits?

Handle Vonage rate limits by implementing client-side throttling (e.g., using the 'p-limit' library to control concurrency) or a job queue system. Start with conservative concurrency limits and adjust based on Vonage limits, testing, and observed 429 error rates. Remember, queuing is best for high volume.

What is the recommended Node.js logger for bulk SMS?

Pino is a highly performant Node.js logger ideal for bulk SMS systems due to its fast, structured JSON logging capabilities. Use 'pino-pretty' in development for readable logs and standard JSON output for production environments, making integration with log management tools easier.

How to implement error handling for bulk SMS with Vonage?

Implement structured error handling by returning consistent objects from your send function indicating success/failure and including error details. Use a global error handler in your Express app to catch and log unexpected exceptions, providing context for debugging.

What is p-limit and how does it work?

p-limit is a Node.js library that allows you to control the concurrency of asynchronous operations. It's essential for throttling outgoing requests in bulk SMS sending to avoid hitting Vonage API rate limits, and works well when combined with Promise.allSettled for asynchronous operations.

How to structure a bulk SMS Node.js project?

Start by creating separate modules for logging (logger.js), Vonage client initialization (vonageClient.js), core SMS sending functions (smsService.js), and database interaction logic (if used). This promotes modularity and improves maintainability.

What is the role of Express.js in bulk SMS architecture?

Express.js is a web framework in Node.js used to create the API endpoints (e.g., /broadcast) that handle incoming requests, manage routing, parse request bodies, and send responses. It provides structure and handles HTTP interactions for the bulk SMS application.

What are the prerequisites for bulk SMS with Vonage?

You'll need a Vonage API account (with API key and secret), a Vonage virtual number or registered alphanumeric Sender ID, Node.js and npm installed, basic understanding of JavaScript and REST APIs, and optional tools like curl or Postman for testing.