How to Send Bulk SMS with Vonage Messages API in Node.js and Express
Step-by-step tutorial: Build a production-ready bulk SMS system using Vonage Messages API, Node.js, and Express. Includes rate limiting, webhooks, 10DLC compliance, and best practices for SMS broadcasting.
This guide provides a step-by-step walkthrough for building a robust bulk SMS broadcasting system using Node.js, Express, and the Vonage Messages API. You'll cover everything from project setup and core Vonage integration to essential production considerations like rate limiting, error handling, security, and deployment.
By the end of this guide, you'll have a functional Node.js application capable of sending SMS messages to a list of recipients via an API endpoint, incorporating best practices for reliability and scalability.
Project Overview and Goals
What You're Building:
You'll construct a Node.js application using the Express framework that exposes an API endpoint. This endpoint accepts a list of phone numbers and a message text, then uses the Vonage Messages API to broadcast the SMS message to all specified recipients efficiently and reliably.
Problem Solved:
This system addresses the need to send the same SMS message to multiple recipients simultaneously (e.g., notifications, alerts, marketing campaigns) while managing API rate limits, tracking message status, and ensuring secure handling of credentials.
Technologies Used:
Node.js: A JavaScript runtime environment ideal for building scalable network applications. This guide targets Node.js v22 Long Term Support (LTS) (Active LTS through April 2027, recommended) or v20 LTS (Maintenance LTS through April 2026). Node.js v18 reaches end-of-life on April 30, 2025.
Express: A minimal and flexible Node.js web application framework for creating the API layer. This guide uses Express v5.1.0 (released October 2024), which requires Node.js 18 or higher and includes important security fixes for Regular Expression Denial of Service (ReDoS) attacks.
Vonage Messages API: A unified API from Vonage for sending messages across various channels, including SMS. Choose the Messages API over the older SMS API for its modern features, better support for different channels, and detailed status webhooks.
dotenv: A zero-dependency module that loads environment variables from a .env file into process.env. Latest version: 17.2.3 (January 2025). Note: For Node.js v20.6.0+, native .env support via --env-file flag is available. dotenv is suitable for development but not recommended for production secrets – use secrets managers like Infisical, AWS Secrets Manager, or similar.
@vonage/server-sdk: The official Vonage Server SDK for Node.js (v3.24.1 as of January 2025), simplifying interaction with Vonage APIs.
ngrok (for development): A tool to expose local servers to the internet, necessary for testing Vonage webhooks. Provides secure HTTPS tunneling, webhook inspection, and replay capabilities.
System Architecture:
(A diagram illustrating the system architecture would typically be placed here, showing client, Node.js app, Vonage API, Mobile Network, and Recipient interactions, including the webhook flow.)
Prerequisites:
A Vonage API account.
Node.js and npm (or yarn) installed locally. Node.js v22 (Active LTS) or v20 (Maintenance LTS) recommended.
Basic understanding of JavaScript, Node.js, and REST APIs (Representational State Transfer Application Programming Interfaces).
ngrok installed for local webhook testing (Download here).
Vonage CLI (Command Line Interface) (Optional but helpful for managing applications/numbers): Run on-demand using npx @vonage/cli [...] or install globally (npm install -g @vonage/cli), which may require administrator privileges.
US Application-to-Person (A2P) 10 Digit Long Code (10DLC) Registration (Mandatory for US SMS): If sending SMS to US recipients, complete 10DLC brand and campaign registration. This process involves vetting by Vonage and carriers and may take several days to weeks. Unregistered long codes will be blocked or heavily filtered. (Source: Vonage API Support, January 2025)
Final Outcome:
A Node.js Express application with:
An API endpoint (/broadcast) to trigger bulk SMS sends.
Integration with the Vonage Messages API using the Node.js SDK.
Basic rate limiting to comply with Vonage API constraints.
Environment variable management for secure credential handling.
Webhook endpoints to receive message status updates from Vonage.
Logging for monitoring and troubleshooting.
How Do You Set Up Your Node.js Project for Bulk SMS?
Initialize your Node.js project and install the necessary dependencies.
Step 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
Step 2: Initialize Node.js Project
Initialize the project using npm (or yarn). This creates a package.json file. The -y flag accepts the default settings.
Bash
npm init -y
Step 3: Install Dependencies
Install Express for the web server, the Vonage Server SDK, and dotenv for environment variables. Version constraints ensure compatibility.
Note on Express v5: Express v5 includes breaking changes from v4, including removal of deprecated methods and updated path-to-regexp@8.x for security (ReDoS mitigation). If migrating from Express v4, review the Express v5 migration guide.
Step 4: Set Up Project Structure
Create the basic files and folders for your application.
Add node_modules and .env to your .gitignore file to prevent committing sensitive information and dependencies to version control.
Text
node_modules
.env
Step 6: Configure Environment Variables (.env)
Create a .env file in the root of your project. This file stores your Vonage credentials and other configuration securely. Fill in the actual values later.
Security Note: dotenv is suitable for local development only. For production environments, use dedicated secrets management solutions (AWS Secrets Manager, Infisical, HashiCorp Vault, etc.) to avoid storing plaintext secrets in environment files. Over 1 million secrets from 58,000+ websites have been exposed through leaked .env files (Source: Security research, 2024).
.env
# Vonage Credentials & Config
VONAGE_API_KEY=YOUR_VONAGE_API_KEY
VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root
VONAGE_FROM_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
# Application Settings
PORT=3000
BASE_URL=http://localhost:3000 # For local testing, will be replaced by ngrok URL for webhooks
Why This Setup?
npm init: Standard way to start a Node.js project, managing dependencies and scripts.
Dependencies: Express is standard for Node.js web apps; @vonage/server-sdk simplifies Vonage interaction; dotenv is crucial for securely managing API keys outside of code.
.gitignore: Prevents accidental exposure of secrets (.env) and unnecessary bloat (node_modules) in Git.
.env: Isolates configuration and secrets from the codebase, making it easier to manage different environments (development, production) and enhancing security.
How Do You Initialize the Vonage SDK and Send SMS?
Now, integrate the Vonage SDK and implement the logic to send SMS messages.
Step 1: Initialize Express and Vonage SDK
Open index.js and set up the basic Express server and initialize the Vonage SDK using the environment variables.
JavaScript
// index.jsrequire('dotenv').config(); // Load environment variables from .env fileconst express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const fs = require('fs'); // Needed to read the private key fileconst app = express();
app.use(express.json()); // Middleware to parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Middleware for URL-encoded bodiesconstPORT = process.env.PORT || 3000;
constVONAGE_FROM_NUMBER = process.env.VONAGE_FROM_NUMBER;
// --- Vonage Initialization ---// Check if required Vonage environment variables are setif (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !VONAGE_FROM_NUMBER) {
console.error('Error: Missing required Vonage environment variables in .env file.');
process.exit(1); // Exit if configuration is missing
}
let vonage;
try {
// Read the private keyconst privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH);
vonage = newVonage({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKey // Use the file content directly
}, {
// Optional: Add custom user agent for tracking/debuggingappendToUserAgent: 'vonage-bulk-sms-guide/1.0.0'
});
console.log('Vonage SDK initialized successfully.');
} catch (error) {
console.error('Error initializing Vonage SDK:', error);
if (error.code === 'ENOENT') {
console.error(`Private key file not found at path: ${process.env.VONAGE_PRIVATE_KEY_PATH}`);
}
process.exit(1);
}
// --- Basic Root Route ---
app.get('/', (req, res) => {
res.send('Vonage Bulk SMS Service is running!');
});
// --- Start Server ---// Export server for testing purposesconst server = app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
console.log(`Base URL: ${process.env.BASE_URL}`);
console.log(`Ensure BASE_URL matches your ngrok forwarding URL for webhooks.`);
});
// --- Placeholder for other routes (API, Webhooks) ---// We will add these later// Export app and server for testingmodule.exports = { app, server, vonage }; // Export vonage if needed for mocking in tests
require('dotenv').config();: Must be called early to load variables.
express.json() / express.urlencoded(): Necessary middleware to parse incoming request bodies.
Vonage Initialization: We use the Vonage class from the SDK, providing credentials and the content of the private key file read using fs.readFileSync. Using the Messages API requires an Application ID and Private Key for authentication.
Error Handling: Added checks for missing environment variables and errors during SDK initialization (like a missing private key file).
Step 2: Implement Single SMS Sending Function
Create a reusable function to send a single SMS message. This helps structure the code. (This version includes enhanced logging).
JavaScript
// index.js (add this function)// --- Function to Send Single SMS (with enhanced logging) ---asyncfunctionsendSms(toNumber, messageText) {
const logPrefix = `[SMS to ${toNumber}]`;
console.log(`${logPrefix} Attempting to send...`);
try {
const resp = await vonage.messages.send({
message_type: "text",
text: messageText,
to: toNumber,
from: VONAGE_FROM_NUMBER, // Must be a Vonage virtual number linked to your applicationchannel: "sms"
});
console.log(`${logPrefix} Submitted successfully. UUID: ${resp.messageUuid}`);
return { success: true, messageUuid: resp.messageUuid, recipient: toNumber };
} catch (err) {
// Extract more specific error details if availableconst errorMessage = err.response?.data?.title || err.response?.data?.detail || err.message || 'Unknown error';
// Try to get a numeric status code, or a specific Vonage code, or 'N/A'const errorCode = err.response?.status?.toString() || err.response?.data?.invalid_parameters?.[0]?.name || 'N/A';
console.error(`${logPrefix} Submission failed. Code: ${errorCode}, Reason: ${errorMessage}`);
// Log the full error object for deeper debugging if needed// console.error(`${logPrefix} Full error object:`, JSON.stringify(err.response?.data || err, null, 2));return { success: false, error: errorMessage, errorCode: errorCode, recipient: toNumber };
}
}
vonage.messages.send: The core method from the SDK for the Messages API.
Parameters: message_type, text, to, from, and channel are required for SMS.
async/await: Used for cleaner handling of the asynchronous API call.
Return Value: Returns an object indicating success/failure, including the messageUuid for tracking and errorCode on failure.
Enhanced Logging: Provides more context in logs and attempts to extract meaningful error codes/messages.
Step 3: Implement Basic Rate Limiting (Delay)
Vonage imposes rate limits. The base limit is 30 API requests per second for all API keys. However, actual SMS throughput depends heavily on:
Number Type: Long Code, Toll-Free, or Short Code
US A2P 10DLC Registration: Mandatory for US messaging. Campaign types determine throughput:
Standard campaigns: Typically 4,500 messages/minute (75 msg/sec) on T-Mobile, varies by AT&T based on Trust Score
Sole Proprietor: 3,000 messages/day, 15 messages/minute
Higher throughput: Available upon request for select use cases (up to 30 msg/sec for sanctioned 10DLCs)
Carrier-Specific Limits: AT&T applies per-minute throughput limits based on campaign type; T-Mobile has per-second limits
Target Country Regulations: International destinations have varying carrier limits
Critical: Exceeding your allowed rate will result in HTTP 429 errors and message blocking/queueing. The simple delay approach below is a basic starting point for low-volume testing only. Production systems must implement proper rate limiting based on your specific 10DLC campaign throughput.
(Source: Vonage API Support - "10 DLC Throughput Limits" and "What is the Throughput Limit for Outbound SMS?", January 2025)
JavaScript
// index.js (add this helper function)// --- Helper function for delays ---functionsleep(ms) {
returnnewPromise(resolve =>setTimeout(resolve, ms));
}
// Define delay in milliseconds.// WARNING: 1100ms (~1 SMS/sec) is a VERY basic starting point for UNREGISTERED long codes (which are now BLOCKED in the US).// Your ACTUAL required delay depends entirely on:// - Your Vonage number type (Long Code, Toll-Free, Short Code)// - US A2P 10DLC registration status and approved campaign throughput (MANDATORY for US)// - Target country regulations and carrier-specific limits// EXCEEDING YOUR ALLOWED RATE WILL LEAD TO ERRORS (429) AND MESSAGE BLOCKING.// For 10DLC Standard campaigns: ~75 msg/sec (T-Mobile), varies for AT&T// For Low Volume/Sole Proprietor: 15 msg/minute maximum// Consult Vonage documentation and your 10DLC campaign details for appropriate limits.constSEND_DELAY_MS = 1100; // EXAMPLE ONLY - ADJUST BASED ON YOUR ACTUAL 10DLC THROUGHPUT LIMITS
Why this approach?
Modularity: Separating the sendSms function makes the code cleaner and reusable.
Messages API: Chosen for its modern features and webhook capabilities.
Rate Limiting (Simple Delay): The sleep function provides a basic mechanism to avoid hitting simple per-second limits. Crucially, this is an oversimplification. Production systems sending moderate to high volume, especially in regulated markets like the US (A2P 10DLC), must account for specific throughput limits tied to number types and 10DLC campaign registration. A simple fixed delay is often inadequate. Consider concurrent sending within verified limits (see Section 5/6) or a background job queue for better management.
How Do You Create a Bulk SMS Broadcast API Endpoint?
Create the /broadcast endpoint to trigger the bulk sending process.
Step 1: Define the /broadcast Route
Add a POST route in index.js that accepts a list of numbers and a message.
JavaScript
// index.js (add this route handler)// --- API Endpoint for Bulk Broadcasting ---
app.post('/broadcast', async (req, res) => {
const { recipients, message } = req.body;
// --- Input Validation ---if (!recipients || !Array.isArray(recipients) || recipients.length === 0) {
return res.status(400).json({ error: 'Invalid or missing "recipients" array in request body.' });
}
if (!message || typeof message !== 'string' || message.trim() === '') {
return res.status(400).json({ error: 'Invalid or missing "message" string in request body.' });
}
// Basic phone number format validation.// WARNING: This regex is very basic and NOT recommended for production.// It doesn't validate E.164 format (missing '+') or handle international variations well.// Use a dedicated library like 'libphonenumber-js' for robust validation.const phoneRegex = /^\d{10,15}$/;
const invalidNumbers = recipients.filter(num => !phoneRegex.test(num.toString()));
if (invalidNumbers.length > 0) {
console.warn(`Warning: Potential invalid phone numbers detected (based on basic check): ${invalidNumbers.join(', ')}`);
// Decide whether to reject or proceed. For production, stricter validation before this point is better.// return res.status(400).json({ error: 'Invalid phone number format detected.', invalidNumbers });
}
console.log(`Received broadcast request for ${recipients.length} recipients.`);
console.log(`Message: "${message}"`);
// --- Process Recipients Sequentially with Delay ---const results = [];
let successCount = 0;
let failureCount = 0;
for (const recipient of recipients) {
const result = awaitsendSms(recipient, message);
results.push(result);
if (result.success) {
successCount++;
} else {
failureCount++;
}
// Wait before sending the next message to respect rate limits (see caveats in Section 2)awaitsleep(SEND_DELAY_MS);
}
console.log(`Broadcast attempt finished. Success: ${successCount}, Failure: ${failureCount}`);
// --- Respond to Client ---// Respond with initial submission results. Actual delivery status comes via webhook.// Status 202 implies processing continues, but here we wait for the loop.// For true async, respond earlier and use background jobs (See Section 6).
res.status(200).json({ // Changed to 200 OK as we wait for the loop here. Use 202 if responding before loop.message: `Broadcast submission process completed for ${recipients.length} recipients. Check webhook or logs for delivery status.`,
summary: {
totalRecipients: recipients.length,
submittedSuccessfully: successCount,
submissionFailures: failureCount,
},
details: results // Provides per-recipient submission status
});
});
Input Validation: Checks for the presence and basic format of recipients and message. Includes a very basic regex for phone numbers with a strong recommendation to use a proper library like libphonenumber-js for production.
Sequential Processing: Iterates through the recipients array.
await sendSms(...): Calls our SMS sending function for each recipient.
await sleep(...): Pauses execution between each send attempt, subject to the caveats mentioned earlier.
Response: Returns a 200 OK status after attempting all submissions in the loop. Includes a summary and detailed results of the submission attempts.
Step 2: Testing the Endpoint (Example using curl)
Make sure your server is running (node index.js). Open another terminal and run the following command (replace YOUR_TEST_PHONE_NUMBER_... with actual E.164 formatted numbers):
Bash
curl -X POST http://localhost:3000/broadcast \
-H "Content-Type: application/json" \
-d '{
"recipients": ["YOUR_TEST_PHONE_NUMBER_1", "YOUR_TEST_PHONE_NUMBER_2"],
"message": "Hello from the Bulk SMS Broadcaster!"
}'
Expected JSON Response (example):
JSON
{"message":"Broadcast submission process completed for 2 recipients. Check webhook or logs for delivery status.","summary":{"totalRecipients":2,"submittedSuccessfully":2,"submissionFailures":0},"details":[{"success":true,"messageUuid":"...","recipient":"...","errorCode":null},{"success":true,"messageUuid":"...","recipient":"...","errorCode":null}]}
(The actual success, messageUuid, recipient, and errorCode values will reflect the outcome of the submission attempts.)
You should also see logs in your server console and receive the SMS messages on the test phones (after the specified delay).
How Do You Set Up Vonage Webhooks for SMS Delivery Status?
Proper configuration in the Vonage Dashboard and setting up webhooks are crucial for the Messages API.
Step 1: Obtain Vonage Credentials
Log in to your Vonage API Dashboard.
Your API Key and API Secret are displayed on the main dashboard page. Copy these into your .env file.
Step 2: Create a Vonage Application
The Messages API requires an Application to handle authentication (via private key) and webhooks.
Navigate to ""Applications"" in the dashboard sidebar.
Click ""Create a new application"".
Give it a name (e.g., ""Bulk SMS Broadcaster"").
Click ""Generate public and private key"". Immediately save the private.key file that downloads. Place this file in your project's root directory (or update VONAGE_PRIVATE_KEY_PATH in .env if you save it elsewhere). The public key is stored by Vonage.
Enable the ""Messages"" capability.
You'll need to provide two webhook URLs (we'll get the YOUR_NGROK_URL part in Step 4):
Inbound URL: Receives incoming messages sent to your Vonage number. Define it as YOUR_NGROK_URL/webhooks/inbound.
Status URL: Receives status updates (delivered, failed, etc.) for messages you send. This is critical. Define it as YOUR_NGROK_URL/webhooks/status.
Click ""Generate new application"".
Copy the generated Application ID into your .env file.
Step 3: Link a Vonage Number
Go back to the Application details page (Applications -> Your Application Name).
Under ""Linked numbers"", click ""Link"" and choose one of your Vonage virtual numbers capable of sending SMS. This number will be used as the FROM number.
Copy this Vonage Virtual Number into the VONAGE_FROM_NUMBER field in your .env file (use E.164 format, e.g., 14155550100).
Step 4: Set up ngrok for Local Webhook Testing
Vonage needs a publicly accessible URL to send webhooks. ngrok creates a secure tunnel to your local machine.
If your server is running, stop it (Ctrl+C).
Open a terminal in your project directory and run ngrok (replace 3000 if your app uses a different port):
Bash
ngrok http 3000
ngrok will display forwarding URLs (e.g., https://random-subdomain.ngrok-free.app). Copy the https URL. This is YOUR_NGROK_URL.
Update Vonage Application: Go back to your Application settings in the Vonage dashboard. Edit the Messages capability URLs:
Inbound URL: YOUR_NGROK_URL/webhooks/inbound
Status URL: YOUR_NGROK_URL/webhooks/status
Save the changes.
Update .env: Set BASE_URL=YOUR_NGROK_URL in your .env file. This helps keep track of the currently active ngrok URL.
Restart your Node.js server: node index.js
Step 5: Implement Webhook Handlers
Add routes in index.js to receive POST requests from Vonage at the URLs you configured.
JavaScript
// index.js (add these route handlers)// --- Webhook Handler for Message Status Updates ---
app.post('/webhooks/status', (req, res) => {
const statusData = req.body;
console.log('Received Status Webhook:', JSON.stringify(statusData, null, 2));
// --- Process the status update ---// Examples: Update a database record, log specific statuses, trigger alertstry {
if (statusData.status === 'delivered') {
console.log(`Message ${statusData.message_uuid} delivered to ${statusData.to} at ${statusData.timestamp}`);
} elseif (statusData.status === 'failed' || statusData.status === 'rejected') {
// Extract error details carefully, structure might vary slightlyconst reason = statusData.error?.reason || statusData.error?.title || 'Unknown';
const code = statusData.error?.code || 'N/A';
console.error(`Message ${statusData.message_uuid} failed for ${statusData.to}. Reason: ${reason} (Code: ${code})`);
// Log full error object if needed for debugging complex failures// console.error('Full error object:', statusData.error);
} else {
console.log(`Message ${statusData.message_uuid} status update: ${statusData.status}`);
}
} catch (error) {
console.error('Error processing status webhook:', error);
// Decide if you still want to send 200 OK even if processing failed internally
}
// --- IMPORTANT: Respond with 200 OK ---// Vonage expects a 200 OK response to acknowledge receipt of the webhook.// Failure to do so will result in retries from Vonage.
res.status(200).send('OK');
});
// --- Webhook Handler for Inbound Messages (Optional) ---
app.post('/webhooks/inbound', (req, res) => {
const inboundData = req.body;
console.log('Received Inbound SMS:', JSON.stringify(inboundData, null, 2));
// --- Process inbound message ---// Example: Log message, trigger auto-reply, etc.try {
console.log(`SMS received from ${inboundData.from} with text: ""${inboundData.text}""`);
// Add your inbound message processing logic here
} catch (error) {
console.error('Error processing inbound webhook:', error);
}
// Respond with 200 OK
res.status(200).send('OK');
});
/webhooks/status: Logs the received status data. You would typically parse this data (message_uuid, status, error, timestamp) to update the delivery status of your sent messages, perhaps in a database. Error field extraction is made slightly more robust. Added basic try...catch.
/webhooks/inbound: Logs incoming messages sent to your Vonage number. Added basic try...catch.
res.status(200).send('OK'): Crucial! Vonage needs this acknowledgment.
Verification: Send a test broadcast using the /broadcast endpoint again. Watch your server logs. You should see:
Logs from /broadcast initiating the sends.
Logs from /webhooks/status showing updates like submitted, delivered, or failed for each message sent (these may arrive with some delay).
How Do You Add Error Handling and Retry Logic for SMS?
Robust applications need solid error handling and logging.
Error Handling:
SDK Errors: The try...catch block in the sendSms function (updated in Section 2) handles errors during the API call. It now attempts to log more detailed error information.
Webhook Errors: Wrap the logic inside webhook handlers in try...catch (as shown in Section 4) to prevent the server from crashing if processing fails. Ensure you still send 200 OK unless it's a fundamental issue receiving the request.
Input Validation: The /broadcast endpoint includes basic validation. Enhance this using libraries like joi or express-validator, and especially libphonenumber-js for phone numbers.
Logging:
Consistency: Use console.log, console.warn, console.error consistently. The enhanced sendSms provides better context.
Information: Log key events: server start, request received, message submission attempts (success/failure with details), webhook received, specific statuses (delivered/failed). Include relevant data like message_uuid, recipient number, timestamps, and error details.
Production Logging: For production, consider using a dedicated logging library like winston or pino for structured logging (JSON), log levels, and transports (files, external services).
Retry Mechanisms:
Vonage Webhooks: Vonage automatically retries sending webhooks if it doesn't receive a 200 OK response. Ensure your webhook handlers are reliable and respond quickly.
Sending SMS: If sendSms fails due to potentially temporary issues (e.g., network timeout, rate limiting error 429), you might implement a limited retry strategy.
Simple Retry: Wrap the sendSms call in a loop with delays.
Exponential Backoff: Increase the delay between retries (e.g., 1s, 2s, 4s). Libraries like async-retry can help.
Example: Simple Retry Logic (Conceptual - Integrate into /broadcast loop)
This shows how you might add retry logic within the /broadcast handler's loop.
JavaScript
// Conceptual - Integrating into the /broadcast loop// Requires careful implementation to avoid infinite loops and manage delaysconstMAX_RETRIES = 2;
constRETRY_DELAY_MS = 2000; // Initial delay for first retryfor (const recipient of recipients) {
let attempt = 0;
let result;
while (attempt <= MAX_RETRIES) {
result = awaitsendSms(recipient, message);
// Check if successful OR if the error is NOT retryableif (result.success || !isRetryableError(result.errorCode)) {
break; // Success or non-retryable error, stop retrying
}
attempt++;
if (attempt <= MAX_RETRIES) {
const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1); // Exponential backoffconsole.warn(`[SMS to ${recipient}] Retrying (${attempt}/${MAX_RETRIES}) after ${delay}ms delay due to error code ${result.errorCode}...`);
awaitsleep(delay);
} else {
console.error(`[SMS to ${recipient}] Max retries reached. Giving up after error code ${result.errorCode}.`);
}
}
results.push(result); // Add final result (success or last failure)// ... rest of loop ...// Main delay between *different* recipients (still subject to Section 2 caveats)if (attempt <= MAX_RETRIES) { // Only delay if we didn't just finish retryingawaitsleep(SEND_DELAY_MS);
}
}
functionisRetryableError(errorCode) {
// Example: Vonage uses 429 for rate limits, 5xx for server issues// Check if errorCode is a string representing a number before parsingconst numericCode = parseInt(errorCode, 10);
if (isNaN(numericCode)) {
returnfalse; // Cannot retry non-numeric error codes this way
}
const retryableCodes = [429, 500, 502, 503, 504]; // Add other transient codes if identifiedreturn retryableCodes.includes(numericCode);
}
Caution: Implement retries carefully. Only retry transient issues (like 429, 5xx). Retrying permanent errors (400 bad number) is wasteful. Ensure delays don't compound excessively. For large scale, offload sending to a background queue system (like BullMQ) which handles retries more robustly. The isRetryableError function now handles potentially non-numeric errorCode values.
How Do You Track SMS Messages with a Database?
For tracking broadcast jobs, individual message statuses, and managing recipients effectively, a database is highly recommended. Here's a conceptual example using Prisma (a popular Node.js ORM).
Step 1: Install Prisma
Bash
npm install prisma@^6.16.0 --save-dev
npm install @prisma/client@^6.16.0
npx prisma init --datasource-provider postgresql # Or sqlite, mysql, etc.
Prisma 6 Updates: Prisma 6.16.3 (January 2025) includes the completed migration from Rust to TypeScript for core logic, a new ESM-first generator splitting Prisma Client into multiple files, and enhanced full-text search capabilities. Minimum supported versions: Node.js 18.18.0+ and TypeScript 5.0+. (Source: Prisma Changelog, January 2025)
This creates a prisma folder with schema.prisma and updates .env with DATABASE_URL.
Step 2: Define Schema (prisma/schema.prisma)
Prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = ""prisma-client-js""
}
datasource db {
provider = ""postgresql"" // Or your chosen provider
url = env(""DATABASE_URL"")
}
model Broadcast {
id String @id @default(cuid())
message String
status String @default(""PENDING"") // PENDING, PROCESSING, COMPLETED, FAILED
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
recipients Recipient[]
}
model Recipient {
id String @id @default(cuid())
phoneNumber String
status String @default(""PENDING"") // PENDING, QUEUED, SUBMITTED, DELIVERED, FAILED, REJECTED
vonageMessageUuid String? @unique // Link to Vonage message
submittedAt DateTime?
lastStatusUpdate DateTime?
errorMessage String?
errorCode String? // Store error code from submission or webhook
broadcast Broadcast @relation(fields: [broadcastId], references: [id])
broadcastId String
@@index([broadcastId])
@@index([status])
@@index([vonageMessageUuid]) // Index for webhook lookup
}
Broadcast: Represents a single bulk sending job.
Recipient: Represents each individual message within a broadcast, tracking its specific status and Vonage messageUuid. Added index on vonageMessageUuid.
Relations: A one-to-many relationship between Broadcast and Recipient.
Indexes: Added for potentially common queries.
Step 3: Apply Schema Changes
Create and apply the first migration.
Bash
npx prisma migrate dev --name init
Step 4: Integrate Prisma Client
Instantiate the client and use it to interact with the database. Handle potential connection errors and ensure graceful shutdown.
JavaScript
// index.js (add near the top)const { PrismaClient } = require('@prisma/client');
const prisma = newPrismaClient();
// Basic check for DB connection on startup (optional but good)asyncfunctioncheckDbConnection() {
try {
await prisma.$connect();
console.log('Database connection successful.');
} catch (error) {
console.error('Database connection failed:', error);
process.exit(1); // Exit if DB is essential
}
}
checkDbConnection(); // Call the check// Graceful shutdown for Prismaasyncfunctionshutdown() {
console.log('Disconnecting database...');
try {
await prisma.$disconnect();
console.log('Database disconnected.');
} catch (e) {
console.error('Error disconnecting database:', e);
} finally {
server.close(() => { // Close HTTP server after DB disconnect attemptconsole.log('Server closed.');
process.exit(0);
});
// Force exit after timeout if server doesn't close gracefullysetTimeout(() => {
console.error('Forcefully shutting down after timeout.');
process.exit(1);
}, 5000); // 5 second timeout
}
}
process.on('SIGTERM', shutdown); // Handle termination signal
process.on('SIGINT', shutdown); // Handle interrupt signal (Ctrl+C)
Added basic connection check and graceful shutdown handlers.
Step 5: Modify API and Webhooks to Use Database
/broadcast Endpoint:
Create a Broadcast record.
Create Recipient records (status PENDING or QUEUED).
Respond 202 Accepted immediately after creating records to indicate processing will happen asynchronously.
Then, iterate through recipients (or preferably, trigger background jobs).
When sendSms is called and returns successfully, update the corresponding Recipient record (status: 'SUBMITTED', vonageMessageUuid, submittedAt).
If sendSms fails, update the Recipient record (status: 'FAILED', errorMessage, errorCode).
/webhooks/status Handler:
Find the Recipient record using the vonageMessageUuid from the webhook data.
Update the Recipient's status, lastStatusUpdate, and potentially errorMessage/errorCode based on the webhook content (delivered, failed, rejected, etc.).
Handle cases where the messageUuid might not be found (e.g., log an error).
(Implementing these database interactions is left as an exercise but involves using prisma.broadcast.create, prisma.recipient.createMany, prisma.recipient.update, and prisma.recipient.findUnique within the respective route handlers.)
Frequently Asked Questions About Vonage Bulk SMS Broadcasting
How do you send bulk SMS messages with Vonage?
Send bulk SMS messages with Vonage by using the Vonage Messages API with Node.js and Express. Create an application with the Vonage Server SDK, authenticate with your API credentials and private key, then iterate through your recipient list calling the vonage.messages.send() method for each phone number. Implement rate limiting based on your 10DLC campaign throughput to avoid HTTP 429 errors.
What is the Vonage Messages API rate limit?
The Vonage Messages API has a base limit of 30 API requests per second. However, actual SMS throughput depends on your number type and US A2P 10DLC registration. Standard 10DLC campaigns typically allow 4,500 messages/minute (75 msg/sec on T-Mobile), while Low Volume campaigns are limited to 15 messages/minute. Exceeding your rate limit results in HTTP 429 errors and message queueing or blocking.
Why use the Vonage Messages API instead of the SMS API?
Use the Vonage Messages API over the older SMS API for modern features including multi-channel support (SMS, WhatsApp, Facebook Messenger, Viber), detailed status webhooks with delivery confirmations, better error reporting, and application-based authentication with private keys. The Messages API provides more robust tracking and supports future channel expansion.
What is US A2P 10DLC registration and why is it mandatory?
US A2P 10DLC (Application-to-Person 10 Digit Long Code) registration is mandatory for all SMS traffic to US recipients using standard long codes. The registration process involves brand and campaign vetting by Vonage and carriers, taking several days to weeks. Unregistered long codes are blocked or heavily filtered by carriers. Registration determines your throughput limits based on campaign type (Standard, Low Volume, or Sole Proprietor).
How do Vonage webhook callbacks work for message status?
Vonage sends webhook callbacks as HTTP POST requests to your configured Status URL whenever a message status changes (submitted, delivered, failed, rejected). Your webhook endpoint receives JSON data including message_uuid, status, timestamp, and error details. Respond with HTTP 200 OK to acknowledge receipt. Vonage automatically retries if it doesn't receive a 200 response.
What Node.js version do you need for Vonage bulk SMS?
Use Node.js v22 LTS (Active LTS through April 2027, recommended) or v20 LTS (Maintenance LTS through April 2026) for Vonage bulk SMS applications. Express v5.1.0 requires Node.js 18 or higher. Avoid Node.js v18 as it reaches end-of-life on April 30, 2025, and won't receive security updates.
How do you handle rate limiting in production?
Handle rate limiting in production by implementing delays between sends based on your 10DLC campaign throughput, using a background job queue (BullMQ, Bull) for asynchronous processing, implementing exponential backoff for HTTP 429 errors, and monitoring carrier-specific limits (AT&T applies per-minute limits, T-Mobile per-second). Track submission attempts in a database to avoid duplicate sends during retries.
What database should you use for tracking SMS broadcasts?
Use PostgreSQL with Prisma ORM (v6.16.3+) for tracking SMS broadcasts. Create tables for Broadcast jobs and Recipients with fields for phoneNumber, status, vonageMessageUuid, submittedAt, lastStatusUpdate, and errorCode. Index the vonageMessageUuid field for fast webhook lookups. Alternative databases include MySQL, MongoDB, or SQLite for development.
How do you secure Vonage API credentials?
Secure Vonage API credentials by storing them in environment variables using dotenv for development, never committing .env files to version control, using secrets managers (AWS Secrets Manager, Infisical, HashiCorp Vault) for production, rotating API keys regularly, and restricting private key file permissions (chmod 600 private.key). Over 1 million secrets have been exposed through leaked .env files.
What are the throughput limits for different 10DLC campaign types?
Throughput limits for 10DLC campaigns vary by type: Standard campaigns allow 4,500 messages/minute (75 msg/sec on T-Mobile), Low Volume Mixed campaigns limit to 1,000 messages/day and 15 messages/minute, Sole Proprietor campaigns allow 3,000 messages/day and 15 messages/minute, and sanctioned 10DLCs can send up to 30 messages/second. AT&T applies per-minute limits based on Trust Score; T-Mobile uses per-second limits.
How do you test Vonage webhooks locally?
Test Vonage webhooks locally using ngrok to create a secure HTTPS tunnel to your localhost. Run ngrok http 3000 to get a public URL, configure your Vonage Application's Status URL and Inbound URL with the ngrok URL plus your webhook paths (e.g., https://abc123.ngrok-free.app/webhooks/status), and use ngrok's web interface to inspect and replay webhook requests for debugging.
What is the difference between synchronous and asynchronous SMS sending?
Synchronous SMS sending processes recipients sequentially in the API request, blocking until all messages are submitted, resulting in long response times for large recipient lists. Asynchronous sending responds immediately with HTTP 202 Accepted, processes sends in background jobs or queues, allows better rate limiting and retry logic, and scales better for production workloads. Use background job queues like BullMQ for production systems.
How do you implement retry logic for failed SMS?
Implement retry logic by catching errors from sendSms(), checking if the error is retryable (HTTP 429, 5xx status codes), using exponential backoff for delays between retries (1s, 2s, 4s, 8s), limiting maximum retry attempts (2-3 retries), and avoiding retries for permanent errors (HTTP 400 bad number format). Log all retry attempts with error codes for debugging and tracking delivery issues.
What Express.js middleware do you need for Vonage?
Essential Express.js middleware for Vonage includes express.json() to parse JSON request bodies, express.urlencoded({ extended: true }) for form data, custom rate limiting middleware or express-rate-limit package, error handling middleware for catching async errors, and logging middleware like morgan or winston for request/response tracking. Express v5+ natively handles promise rejections from middleware.
How do you validate phone numbers before sending SMS?
Validate phone numbers using the libphonenumber-js library to check E.164 format (e.g., +14155550100), verify the number is valid for SMS, extract country code and national number, format numbers consistently, and check against carrier databases. Avoid basic regex validation in production as it doesn't handle international variations, special cases, or invalid number ranges accurately.
Frequently asked questions
Use Node.js with Express and the Vonage Messages API to create a bulk SMS system. This involves setting up an API endpoint that accepts a list of numbers and a message, then uses the Vonage SDK to send the messages. Ensure proper rate limiting and error handling for reliability.
The Vonage Messages API is a unified API that allows you to send messages through different channels, including SMS. It's preferred over the older SMS API due to its modern design, wider channel support, and detailed status updates via webhooks.
Dotenv is a module used to load environment variables from a .env file. This helps keep sensitive information like Vonage API keys and secrets separate from your codebase, improving security.
Ngrok is used during development to create a publicly accessible URL for your local server. This allows Vonage to send webhooks to your application for testing purposes. In production, you'd use your actual server URL.
While the Vonage SMS API can still be used, the Messages API is recommended due to its versatility and features. The Messages API supports multiple communication channels and provides detailed status tracking through webhooks.
Vonage has rate limits to prevent abuse. A basic approach involves implementing delays between sending messages. However, more robust solutions are needed for production applications. Check Vonage documentation for your number type and registration status.
The status webhook provides updates on the delivery status of your messages. This allows real-time tracking. You must configure a status URL in your Vonage Application settings and implement a handler in your application.
You can install the Vonage Server SDK for Node.js using npm or yarn: 'npm install @vonage/server-sdk'. This gives you access to convenient methods for interacting with Vonage APIs.
The private key, along with your application ID, is essential for authenticating with the Vonage Messages API. This ensures secure communication and access to your account.
In the Vonage dashboard, create a new application, generate public and private keys (saving the private key securely), enable the 'Messages' capability, and link a Vonage virtual number. Set the inbound and status webhook URLs to your application endpoints.
Use try-catch blocks around API calls and webhook processing, validate input data, and log all relevant events and errors. Consider implementing retry mechanisms for transient errors and use robust logging libraries in production.
While basic regex is possible, use a library like 'libphonenumber-js' for robust validation, especially to ensure compliance with international number formats (E.164). This prevents invalid number submissions.
A database is recommended to track broadcast jobs, individual message statuses, and manage recipients effectively. This enables reporting, monitoring delivery statistics, and retrying failed messages if necessary.
The JSON payload should include two main fields: 'recipients', an array of phone numbers in E.164 format, and 'message', a string containing the text to send. Ensure correct number formatting.
This guide provides a step-by-step walkthrough for building a robust bulk SMS broadcasting system using Node.js, Express, and the Vonage Messages API. You'll cover everything from project setup and core Vonage integration to essential production considerations like rate limiting, error handling, security, and deployment.
By the end of this guide, you'll have a functional Node.js application capable of sending SMS messages to a list of recipients via an API endpoint, incorporating best practices for reliability and scalability.
Project Overview and Goals
What You're Building:
You'll construct a Node.js application using the Express framework that exposes an API endpoint. This endpoint accepts a list of phone numbers and a message text, then uses the Vonage Messages API to broadcast the SMS message to all specified recipients efficiently and reliably.
Problem Solved:
This system addresses the need to send the same SMS message to multiple recipients simultaneously (e.g., notifications, alerts, marketing campaigns) while managing API rate limits, tracking message status, and ensuring secure handling of credentials.
Technologies Used:
dotenv: A zero-dependency module that loads environment variables from a.envfile intoprocess.env. Latest version: 17.2.3 (January 2025). Note: For Node.js v20.6.0+, native.envsupport via--env-fileflag is available. dotenv is suitable for development but not recommended for production secrets – use secrets managers like Infisical, AWS Secrets Manager, or similar.@vonage/server-sdk: The official Vonage Server SDK for Node.js (v3.24.1 as of January 2025), simplifying interaction with Vonage APIs.ngrok(for development): A tool to expose local servers to the internet, necessary for testing Vonage webhooks. Provides secure HTTPS tunneling, webhook inspection, and replay capabilities.System Architecture:
(A diagram illustrating the system architecture would typically be placed here, showing client, Node.js app, Vonage API, Mobile Network, and Recipient interactions, including the webhook flow.)
Prerequisites:
ngrokinstalled for local webhook testing (Download here).npx @vonage/cli [...]or install globally (npm install -g @vonage/cli), which may require administrator privileges.Final Outcome:
A Node.js Express application with:
/broadcast) to trigger bulk SMS sends.How Do You Set Up Your Node.js Project for Bulk SMS?
Initialize your Node.js project and install the necessary dependencies.
Step 1: Create Project Directory
Open your terminal and create a new directory for the project, then navigate into it.
mkdir vonage-bulk-sms cd vonage-bulk-smsStep 2: Initialize Node.js Project
Initialize the project using npm (or yarn). This creates a
package.jsonfile. The-yflag accepts the default settings.Step 3: Install Dependencies
Install Express for the web server, the Vonage Server SDK, and
dotenvfor environment variables. Version constraints ensure compatibility.Note on Express v5: Express v5 includes breaking changes from v4, including removal of deprecated methods and updated path-to-regexp@8.x for security (ReDoS mitigation). If migrating from Express v4, review the Express v5 migration guide.
Step 4: Set Up Project Structure
Create the basic files and folders for your application.
touch index.js .env .gitignoreYour basic structure should look like this:
Step 5: Configure
.gitignoreAdd
node_modulesand.envto your.gitignorefile to prevent committing sensitive information and dependencies to version control.Step 6: Configure Environment Variables (
.env)Create a
.envfile in the root of your project. This file stores your Vonage credentials and other configuration securely. Fill in the actual values later.Security Note: dotenv is suitable for local development only. For production environments, use dedicated secrets management solutions (AWS Secrets Manager, Infisical, HashiCorp Vault, etc.) to avoid storing plaintext secrets in environment files. Over 1 million secrets from 58,000+ websites have been exposed through leaked .env files (Source: Security research, 2024).
Why This Setup?
npm init: Standard way to start a Node.js project, managing dependencies and scripts.@vonage/server-sdksimplifies Vonage interaction;dotenvis crucial for securely managing API keys outside of code..gitignore: Prevents accidental exposure of secrets (.env) and unnecessary bloat (node_modules) in Git..env: Isolates configuration and secrets from the codebase, making it easier to manage different environments (development, production) and enhancing security.How Do You Initialize the Vonage SDK and Send SMS?
Now, integrate the Vonage SDK and implement the logic to send SMS messages.
Step 1: Initialize Express and Vonage SDK
Open
index.jsand set up the basic Express server and initialize the Vonage SDK using the environment variables.// index.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); const fs = require('fs'); // Needed to read the private key file const app = express(); app.use(express.json()); // Middleware to parse JSON bodies app.use(express.urlencoded({ extended: true })); // Middleware for URL-encoded bodies const PORT = process.env.PORT || 3000; const VONAGE_FROM_NUMBER = process.env.VONAGE_FROM_NUMBER; // --- Vonage Initialization --- // Check if required Vonage environment variables are set if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !VONAGE_FROM_NUMBER) { console.error('Error: Missing required Vonage environment variables in .env file.'); process.exit(1); // Exit if configuration is missing } let vonage; try { // Read the private key const privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH); vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKey // Use the file content directly }, { // Optional: Add custom user agent for tracking/debugging appendToUserAgent: 'vonage-bulk-sms-guide/1.0.0' }); console.log('Vonage SDK initialized successfully.'); } catch (error) { console.error('Error initializing Vonage SDK:', error); if (error.code === 'ENOENT') { console.error(`Private key file not found at path: ${process.env.VONAGE_PRIVATE_KEY_PATH}`); } process.exit(1); } // --- Basic Root Route --- app.get('/', (req, res) => { res.send('Vonage Bulk SMS Service is running!'); }); // --- Start Server --- // Export server for testing purposes const server = app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); console.log(`Base URL: ${process.env.BASE_URL}`); console.log(`Ensure BASE_URL matches your ngrok forwarding URL for webhooks.`); }); // --- Placeholder for other routes (API, Webhooks) --- // We will add these later // Export app and server for testing module.exports = { app, server, vonage }; // Export vonage if needed for mocking in testsrequire('dotenv').config();: Must be called early to load variables.express.json()/express.urlencoded(): Necessary middleware to parse incoming request bodies.Vonageclass from the SDK, providing credentials and the content of the private key file read usingfs.readFileSync. Using the Messages API requires an Application ID and Private Key for authentication.Step 2: Implement Single SMS Sending Function
Create a reusable function to send a single SMS message. This helps structure the code. (This version includes enhanced logging).
// index.js (add this function) // --- Function to Send Single SMS (with enhanced logging) --- async function sendSms(toNumber, messageText) { const logPrefix = `[SMS to ${toNumber}]`; console.log(`${logPrefix} Attempting to send...`); try { const resp = await vonage.messages.send({ message_type: "text", text: messageText, to: toNumber, from: VONAGE_FROM_NUMBER, // Must be a Vonage virtual number linked to your application channel: "sms" }); console.log(`${logPrefix} Submitted successfully. UUID: ${resp.messageUuid}`); return { success: true, messageUuid: resp.messageUuid, recipient: toNumber }; } catch (err) { // Extract more specific error details if available const errorMessage = err.response?.data?.title || err.response?.data?.detail || err.message || 'Unknown error'; // Try to get a numeric status code, or a specific Vonage code, or 'N/A' const errorCode = err.response?.status?.toString() || err.response?.data?.invalid_parameters?.[0]?.name || 'N/A'; console.error(`${logPrefix} Submission failed. Code: ${errorCode}, Reason: ${errorMessage}`); // Log the full error object for deeper debugging if needed // console.error(`${logPrefix} Full error object:`, JSON.stringify(err.response?.data || err, null, 2)); return { success: false, error: errorMessage, errorCode: errorCode, recipient: toNumber }; } }vonage.messages.send: The core method from the SDK for the Messages API.message_type,text,to,from, andchannelare required for SMS.async/await: Used for cleaner handling of the asynchronous API call.messageUuidfor tracking anderrorCodeon failure.Step 3: Implement Basic Rate Limiting (Delay)
Vonage imposes rate limits. The base limit is 30 API requests per second for all API keys. However, actual SMS throughput depends heavily on:
Critical: Exceeding your allowed rate will result in HTTP 429 errors and message blocking/queueing. The simple delay approach below is a basic starting point for low-volume testing only. Production systems must implement proper rate limiting based on your specific 10DLC campaign throughput.
(Source: Vonage API Support - "10 DLC Throughput Limits" and "What is the Throughput Limit for Outbound SMS?", January 2025)
// index.js (add this helper function) // --- Helper function for delays --- function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Define delay in milliseconds. // WARNING: 1100ms (~1 SMS/sec) is a VERY basic starting point for UNREGISTERED long codes (which are now BLOCKED in the US). // Your ACTUAL required delay depends entirely on: // - Your Vonage number type (Long Code, Toll-Free, Short Code) // - US A2P 10DLC registration status and approved campaign throughput (MANDATORY for US) // - Target country regulations and carrier-specific limits // EXCEEDING YOUR ALLOWED RATE WILL LEAD TO ERRORS (429) AND MESSAGE BLOCKING. // For 10DLC Standard campaigns: ~75 msg/sec (T-Mobile), varies for AT&T // For Low Volume/Sole Proprietor: 15 msg/minute maximum // Consult Vonage documentation and your 10DLC campaign details for appropriate limits. const SEND_DELAY_MS = 1100; // EXAMPLE ONLY - ADJUST BASED ON YOUR ACTUAL 10DLC THROUGHPUT LIMITSWhy this approach?
sendSmsfunction makes the code cleaner and reusable.sleepfunction provides a basic mechanism to avoid hitting simple per-second limits. Crucially, this is an oversimplification. Production systems sending moderate to high volume, especially in regulated markets like the US (A2P 10DLC), must account for specific throughput limits tied to number types and 10DLC campaign registration. A simple fixed delay is often inadequate. Consider concurrent sending within verified limits (see Section 5/6) or a background job queue for better management.How Do You Create a Bulk SMS Broadcast API Endpoint?
Create the
/broadcastendpoint to trigger the bulk sending process.Step 1: Define the
/broadcastRouteAdd a POST route in
index.jsthat accepts a list of numbers and a message.// index.js (add this route handler) // --- API Endpoint for Bulk Broadcasting --- app.post('/broadcast', async (req, res) => { const { recipients, message } = req.body; // --- Input Validation --- if (!recipients || !Array.isArray(recipients) || recipients.length === 0) { return res.status(400).json({ error: 'Invalid or missing "recipients" array in request body.' }); } if (!message || typeof message !== 'string' || message.trim() === '') { return res.status(400).json({ error: 'Invalid or missing "message" string in request body.' }); } // Basic phone number format validation. // WARNING: This regex is very basic and NOT recommended for production. // It doesn't validate E.164 format (missing '+') or handle international variations well. // Use a dedicated library like 'libphonenumber-js' for robust validation. const phoneRegex = /^\d{10,15}$/; const invalidNumbers = recipients.filter(num => !phoneRegex.test(num.toString())); if (invalidNumbers.length > 0) { console.warn(`Warning: Potential invalid phone numbers detected (based on basic check): ${invalidNumbers.join(', ')}`); // Decide whether to reject or proceed. For production, stricter validation before this point is better. // return res.status(400).json({ error: 'Invalid phone number format detected.', invalidNumbers }); } console.log(`Received broadcast request for ${recipients.length} recipients.`); console.log(`Message: "${message}"`); // --- Process Recipients Sequentially with Delay --- const results = []; let successCount = 0; let failureCount = 0; for (const recipient of recipients) { const result = await sendSms(recipient, message); results.push(result); if (result.success) { successCount++; } else { failureCount++; } // Wait before sending the next message to respect rate limits (see caveats in Section 2) await sleep(SEND_DELAY_MS); } console.log(`Broadcast attempt finished. Success: ${successCount}, Failure: ${failureCount}`); // --- Respond to Client --- // Respond with initial submission results. Actual delivery status comes via webhook. // Status 202 implies processing continues, but here we wait for the loop. // For true async, respond earlier and use background jobs (See Section 6). res.status(200).json({ // Changed to 200 OK as we wait for the loop here. Use 202 if responding before loop. message: `Broadcast submission process completed for ${recipients.length} recipients. Check webhook or logs for delivery status.`, summary: { totalRecipients: recipients.length, submittedSuccessfully: successCount, submissionFailures: failureCount, }, details: results // Provides per-recipient submission status }); });recipientsandmessage. Includes a very basic regex for phone numbers with a strong recommendation to use a proper library likelibphonenumber-jsfor production.recipientsarray.await sendSms(...): Calls our SMS sending function for each recipient.await sleep(...): Pauses execution between each send attempt, subject to the caveats mentioned earlier.200 OKstatus after attempting all submissions in the loop. Includes a summary and detailed results of the submission attempts.Step 2: Testing the Endpoint (Example using
curl)Make sure your server is running (
node index.js). Open another terminal and run the following command (replaceYOUR_TEST_PHONE_NUMBER_...with actual E.164 formatted numbers):curl -X POST http://localhost:3000/broadcast \ -H "Content-Type: application/json" \ -d '{ "recipients": ["YOUR_TEST_PHONE_NUMBER_1", "YOUR_TEST_PHONE_NUMBER_2"], "message": "Hello from the Bulk SMS Broadcaster!" }'Expected JSON Response (example):
{ "message": "Broadcast submission process completed for 2 recipients. Check webhook or logs for delivery status.", "summary": { "totalRecipients": 2, "submittedSuccessfully": 2, "submissionFailures": 0 }, "details": [ { "success": true, "messageUuid": "...", "recipient": "...", "errorCode": null }, { "success": true, "messageUuid": "...", "recipient": "...", "errorCode": null } ] }(The actual
success,messageUuid,recipient, anderrorCodevalues will reflect the outcome of the submission attempts.)You should also see logs in your server console and receive the SMS messages on the test phones (after the specified delay).
How Do You Set Up Vonage Webhooks for SMS Delivery Status?
Proper configuration in the Vonage Dashboard and setting up webhooks are crucial for the Messages API.
Step 1: Obtain Vonage Credentials
.envfile.Step 2: Create a Vonage Application
The Messages API requires an Application to handle authentication (via private key) and webhooks.
private.keyfile that downloads. Place this file in your project's root directory (or updateVONAGE_PRIVATE_KEY_PATHin.envif you save it elsewhere). The public key is stored by Vonage.YOUR_NGROK_URLpart in Step 4):YOUR_NGROK_URL/webhooks/inbound.YOUR_NGROK_URL/webhooks/status..envfile.Step 3: Link a Vonage Number
FROMnumber.VONAGE_FROM_NUMBERfield in your.envfile (use E.164 format, e.g.,14155550100).Step 4: Set up
ngrokfor Local Webhook TestingVonage needs a publicly accessible URL to send webhooks.
ngrokcreates a secure tunnel to your local machine.ngrok(replace3000if your app uses a different port):ngrokwill display forwarding URLs (e.g.,https://random-subdomain.ngrok-free.app). Copy thehttpsURL. This isYOUR_NGROK_URL.YOUR_NGROK_URL/webhooks/inboundYOUR_NGROK_URL/webhooks/status.env: SetBASE_URL=YOUR_NGROK_URLin your.envfile. This helps keep track of the currently activengrokURL.node index.jsStep 5: Implement Webhook Handlers
Add routes in
index.jsto receive POST requests from Vonage at the URLs you configured.// index.js (add these route handlers) // --- Webhook Handler for Message Status Updates --- app.post('/webhooks/status', (req, res) => { const statusData = req.body; console.log('Received Status Webhook:', JSON.stringify(statusData, null, 2)); // --- Process the status update --- // Examples: Update a database record, log specific statuses, trigger alerts try { if (statusData.status === 'delivered') { console.log(`Message ${statusData.message_uuid} delivered to ${statusData.to} at ${statusData.timestamp}`); } else if (statusData.status === 'failed' || statusData.status === 'rejected') { // Extract error details carefully, structure might vary slightly const reason = statusData.error?.reason || statusData.error?.title || 'Unknown'; const code = statusData.error?.code || 'N/A'; console.error(`Message ${statusData.message_uuid} failed for ${statusData.to}. Reason: ${reason} (Code: ${code})`); // Log full error object if needed for debugging complex failures // console.error('Full error object:', statusData.error); } else { console.log(`Message ${statusData.message_uuid} status update: ${statusData.status}`); } } catch (error) { console.error('Error processing status webhook:', error); // Decide if you still want to send 200 OK even if processing failed internally } // --- IMPORTANT: Respond with 200 OK --- // Vonage expects a 200 OK response to acknowledge receipt of the webhook. // Failure to do so will result in retries from Vonage. res.status(200).send('OK'); }); // --- Webhook Handler for Inbound Messages (Optional) --- app.post('/webhooks/inbound', (req, res) => { const inboundData = req.body; console.log('Received Inbound SMS:', JSON.stringify(inboundData, null, 2)); // --- Process inbound message --- // Example: Log message, trigger auto-reply, etc. try { console.log(`SMS received from ${inboundData.from} with text: ""${inboundData.text}""`); // Add your inbound message processing logic here } catch (error) { console.error('Error processing inbound webhook:', error); } // Respond with 200 OK res.status(200).send('OK'); });/webhooks/status: Logs the received status data. You would typically parse this data (message_uuid,status,error,timestamp) to update the delivery status of your sent messages, perhaps in a database. Error field extraction is made slightly more robust. Added basictry...catch./webhooks/inbound: Logs incoming messages sent to your Vonage number. Added basictry...catch.res.status(200).send('OK'): Crucial! Vonage needs this acknowledgment.Verification: Send a test broadcast using the
/broadcastendpoint again. Watch your server logs. You should see:/broadcastinitiating the sends./webhooks/statusshowing updates likesubmitted,delivered, orfailedfor each message sent (these may arrive with some delay).How Do You Add Error Handling and Retry Logic for SMS?
Robust applications need solid error handling and logging.
Error Handling:
try...catchblock in thesendSmsfunction (updated in Section 2) handles errors during the API call. It now attempts to log more detailed error information.try...catch(as shown in Section 4) to prevent the server from crashing if processing fails. Ensure you still send200 OKunless it's a fundamental issue receiving the request./broadcastendpoint includes basic validation. Enhance this using libraries likejoiorexpress-validator, and especiallylibphonenumber-jsfor phone numbers.Logging:
console.log,console.warn,console.errorconsistently. The enhancedsendSmsprovides better context.message_uuid, recipient number, timestamps, and error details.winstonorpinofor structured logging (JSON), log levels, and transports (files, external services).Retry Mechanisms:
200 OKresponse. Ensure your webhook handlers are reliable and respond quickly.sendSmsfails due to potentially temporary issues (e.g., network timeout, rate limiting error429), you might implement a limited retry strategy.sendSmscall in a loop with delays.async-retrycan help.Example: Simple Retry Logic (Conceptual - Integrate into
/broadcastloop)This shows how you might add retry logic within the
/broadcasthandler's loop.// Conceptual - Integrating into the /broadcast loop // Requires careful implementation to avoid infinite loops and manage delays const MAX_RETRIES = 2; const RETRY_DELAY_MS = 2000; // Initial delay for first retry for (const recipient of recipients) { let attempt = 0; let result; while (attempt <= MAX_RETRIES) { result = await sendSms(recipient, message); // Check if successful OR if the error is NOT retryable if (result.success || !isRetryableError(result.errorCode)) { break; // Success or non-retryable error, stop retrying } attempt++; if (attempt <= MAX_RETRIES) { const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1); // Exponential backoff console.warn(`[SMS to ${recipient}] Retrying (${attempt}/${MAX_RETRIES}) after ${delay}ms delay due to error code ${result.errorCode}...`); await sleep(delay); } else { console.error(`[SMS to ${recipient}] Max retries reached. Giving up after error code ${result.errorCode}.`); } } results.push(result); // Add final result (success or last failure) // ... rest of loop ... // Main delay between *different* recipients (still subject to Section 2 caveats) if (attempt <= MAX_RETRIES) { // Only delay if we didn't just finish retrying await sleep(SEND_DELAY_MS); } } function isRetryableError(errorCode) { // Example: Vonage uses 429 for rate limits, 5xx for server issues // Check if errorCode is a string representing a number before parsing const numericCode = parseInt(errorCode, 10); if (isNaN(numericCode)) { return false; // Cannot retry non-numeric error codes this way } const retryableCodes = [429, 500, 502, 503, 504]; // Add other transient codes if identified return retryableCodes.includes(numericCode); }429,5xx). Retrying permanent errors (400bad number) is wasteful. Ensure delays don't compound excessively. For large scale, offload sending to a background queue system (like BullMQ) which handles retries more robustly. TheisRetryableErrorfunction now handles potentially non-numericerrorCodevalues.How Do You Track SMS Messages with a Database?
For tracking broadcast jobs, individual message statuses, and managing recipients effectively, a database is highly recommended. Here's a conceptual example using Prisma (a popular Node.js ORM).
Step 1: Install Prisma
npm install prisma@^6.16.0 --save-dev npm install @prisma/client@^6.16.0 npx prisma init --datasource-provider postgresql # Or sqlite, mysql, etc.Prisma 6 Updates: Prisma 6.16.3 (January 2025) includes the completed migration from Rust to TypeScript for core logic, a new ESM-first generator splitting Prisma Client into multiple files, and enhanced full-text search capabilities. Minimum supported versions: Node.js 18.18.0+ and TypeScript 5.0+. (Source: Prisma Changelog, January 2025)
This creates a
prismafolder withschema.prismaand updates.envwithDATABASE_URL.Step 2: Define Schema (
prisma/schema.prisma)Broadcast: Represents a single bulk sending job.Recipient: Represents each individual message within a broadcast, tracking its specific status and VonagemessageUuid. Added index onvonageMessageUuid.BroadcastandRecipient.Step 3: Apply Schema Changes
Create and apply the first migration.
Step 4: Integrate Prisma Client
Instantiate the client and use it to interact with the database. Handle potential connection errors and ensure graceful shutdown.
// index.js (add near the top) const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); // Basic check for DB connection on startup (optional but good) async function checkDbConnection() { try { await prisma.$connect(); console.log('Database connection successful.'); } catch (error) { console.error('Database connection failed:', error); process.exit(1); // Exit if DB is essential } } checkDbConnection(); // Call the check // Graceful shutdown for Prisma async function shutdown() { console.log('Disconnecting database...'); try { await prisma.$disconnect(); console.log('Database disconnected.'); } catch (e) { console.error('Error disconnecting database:', e); } finally { server.close(() => { // Close HTTP server after DB disconnect attempt console.log('Server closed.'); process.exit(0); }); // Force exit after timeout if server doesn't close gracefully setTimeout(() => { console.error('Forcefully shutting down after timeout.'); process.exit(1); }, 5000); // 5 second timeout } } process.on('SIGTERM', shutdown); // Handle termination signal process.on('SIGINT', shutdown); // Handle interrupt signal (Ctrl+C)Step 5: Modify API and Webhooks to Use Database
Broadcastrecord.Recipientrecords (statusPENDINGorQUEUED).202 Acceptedimmediately after creating records to indicate processing will happen asynchronously.sendSmsis called and returns successfully, update the correspondingRecipientrecord (status: 'SUBMITTED',vonageMessageUuid,submittedAt).sendSmsfails, update theRecipientrecord (status: 'FAILED',errorMessage,errorCode).Recipientrecord using thevonageMessageUuidfrom the webhook data.Recipient'sstatus,lastStatusUpdate, and potentiallyerrorMessage/errorCodebased on the webhook content (delivered,failed,rejected, etc.).messageUuidmight not be found (e.g., log an error).(Implementing these database interactions is left as an exercise but involves using
prisma.broadcast.create,prisma.recipient.createMany,prisma.recipient.update, andprisma.recipient.findUniquewithin the respective route handlers.)Frequently Asked Questions About Vonage Bulk SMS Broadcasting
How do you send bulk SMS messages with Vonage?
Send bulk SMS messages with Vonage by using the Vonage Messages API with Node.js and Express. Create an application with the Vonage Server SDK, authenticate with your API credentials and private key, then iterate through your recipient list calling the
vonage.messages.send()method for each phone number. Implement rate limiting based on your 10DLC campaign throughput to avoid HTTP 429 errors.What is the Vonage Messages API rate limit?
The Vonage Messages API has a base limit of 30 API requests per second. However, actual SMS throughput depends on your number type and US A2P 10DLC registration. Standard 10DLC campaigns typically allow 4,500 messages/minute (75 msg/sec on T-Mobile), while Low Volume campaigns are limited to 15 messages/minute. Exceeding your rate limit results in HTTP 429 errors and message queueing or blocking.
Why use the Vonage Messages API instead of the SMS API?
Use the Vonage Messages API over the older SMS API for modern features including multi-channel support (SMS, WhatsApp, Facebook Messenger, Viber), detailed status webhooks with delivery confirmations, better error reporting, and application-based authentication with private keys. The Messages API provides more robust tracking and supports future channel expansion.
What is US A2P 10DLC registration and why is it mandatory?
US A2P 10DLC (Application-to-Person 10 Digit Long Code) registration is mandatory for all SMS traffic to US recipients using standard long codes. The registration process involves brand and campaign vetting by Vonage and carriers, taking several days to weeks. Unregistered long codes are blocked or heavily filtered by carriers. Registration determines your throughput limits based on campaign type (Standard, Low Volume, or Sole Proprietor).
How do Vonage webhook callbacks work for message status?
Vonage sends webhook callbacks as HTTP POST requests to your configured Status URL whenever a message status changes (submitted, delivered, failed, rejected). Your webhook endpoint receives JSON data including
message_uuid,status,timestamp, and error details. Respond with HTTP 200 OK to acknowledge receipt. Vonage automatically retries if it doesn't receive a 200 response.What Node.js version do you need for Vonage bulk SMS?
Use Node.js v22 LTS (Active LTS through April 2027, recommended) or v20 LTS (Maintenance LTS through April 2026) for Vonage bulk SMS applications. Express v5.1.0 requires Node.js 18 or higher. Avoid Node.js v18 as it reaches end-of-life on April 30, 2025, and won't receive security updates.
How do you handle rate limiting in production?
Handle rate limiting in production by implementing delays between sends based on your 10DLC campaign throughput, using a background job queue (BullMQ, Bull) for asynchronous processing, implementing exponential backoff for HTTP 429 errors, and monitoring carrier-specific limits (AT&T applies per-minute limits, T-Mobile per-second). Track submission attempts in a database to avoid duplicate sends during retries.
What database should you use for tracking SMS broadcasts?
Use PostgreSQL with Prisma ORM (v6.16.3+) for tracking SMS broadcasts. Create tables for Broadcast jobs and Recipients with fields for
phoneNumber,status,vonageMessageUuid,submittedAt,lastStatusUpdate, anderrorCode. Index thevonageMessageUuidfield for fast webhook lookups. Alternative databases include MySQL, MongoDB, or SQLite for development.How do you secure Vonage API credentials?
Secure Vonage API credentials by storing them in environment variables using dotenv for development, never committing
.envfiles to version control, using secrets managers (AWS Secrets Manager, Infisical, HashiCorp Vault) for production, rotating API keys regularly, and restricting private key file permissions (chmod 600 private.key). Over 1 million secrets have been exposed through leaked .env files.What are the throughput limits for different 10DLC campaign types?
Throughput limits for 10DLC campaigns vary by type: Standard campaigns allow 4,500 messages/minute (75 msg/sec on T-Mobile), Low Volume Mixed campaigns limit to 1,000 messages/day and 15 messages/minute, Sole Proprietor campaigns allow 3,000 messages/day and 15 messages/minute, and sanctioned 10DLCs can send up to 30 messages/second. AT&T applies per-minute limits based on Trust Score; T-Mobile uses per-second limits.
How do you test Vonage webhooks locally?
Test Vonage webhooks locally using ngrok to create a secure HTTPS tunnel to your localhost. Run
ngrok http 3000to get a public URL, configure your Vonage Application's Status URL and Inbound URL with the ngrok URL plus your webhook paths (e.g.,https://abc123.ngrok-free.app/webhooks/status), and use ngrok's web interface to inspect and replay webhook requests for debugging.What is the difference between synchronous and asynchronous SMS sending?
Synchronous SMS sending processes recipients sequentially in the API request, blocking until all messages are submitted, resulting in long response times for large recipient lists. Asynchronous sending responds immediately with HTTP 202 Accepted, processes sends in background jobs or queues, allows better rate limiting and retry logic, and scales better for production workloads. Use background job queues like BullMQ for production systems.
How do you implement retry logic for failed SMS?
Implement retry logic by catching errors from
sendSms(), checking if the error is retryable (HTTP 429, 5xx status codes), using exponential backoff for delays between retries (1s, 2s, 4s, 8s), limiting maximum retry attempts (2-3 retries), and avoiding retries for permanent errors (HTTP 400 bad number format). Log all retry attempts with error codes for debugging and tracking delivery issues.What Express.js middleware do you need for Vonage?
Essential Express.js middleware for Vonage includes
express.json()to parse JSON request bodies,express.urlencoded({ extended: true })for form data, custom rate limiting middleware orexpress-rate-limitpackage, error handling middleware for catching async errors, and logging middleware likemorganorwinstonfor request/response tracking. Express v5+ natively handles promise rejections from middleware.How do you validate phone numbers before sending SMS?
Validate phone numbers using the
libphonenumber-jslibrary to check E.164 format (e.g., +14155550100), verify the number is valid for SMS, extract country code and national number, format numbers consistently, and check against carrier databases. Avoid basic regex validation in production as it doesn't handle international variations, special cases, or invalid number ranges accurately.