code examples
code examples
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:
- Accept a list of phone numbers and a message payload.
- Iterate through the list and send the message to each recipient using the Vonage SMS API.
- Handle Vonage API rate limits gracefully (through client-side throttling or queuing).
- Provide structured logging for successful sends and errors.
- 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
.envfile intoprocess.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)
curlor 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) -->-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
bashmkdir vonage-bulk-sms cd vonage-bulk-sms -
Initialize Node.js Project: This creates a
package.jsonfile to manage dependencies and project metadata.bashnpm init -y -
Install Dependencies: Install Express, the Vonage SDK,
dotenv,pinofor logging, and optionallypino-prettyfor development. Install the latest versions.bashnpm install express @vonage/server-sdk dotenv pino npm install --save-dev pino-pretty # For development logging onlyexpress: 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.envfile.pino: Fast, structured JSON logger.pino-pretty: (Dev dependency) Formats Pino logs for readability during development.
-
Enable ES Modules (Optional but Recommended): To use modern
import/exportsyntax, open yourpackage.jsonfile 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 } } -
Create Core Files: Create the main application file, logger configuration, service files, and environment file.
bashtouch index.js logger.js vonageClient.js smsService.js .env -
Configure Environment Variables (
.env): Open the.envfile 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-hereVONAGE_API_KEY,VONAGE_API_SECRET,VONAGE_SENDER_ID: Your Vonage details. For sending to US numbers,VONAGE_SENDER_IDmust 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.
-
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; -
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
devscript is added topackage.jsonto pipe output throughpino-pretty.
- We import and use our
-
Run the Application: Start the server using the dev script for readable logs.
bashnpm run devYou should see "Server running on http://localhost:3000". Test the health check:
curl http://localhost:3000/health.
2. Implementing Core Functionality: Sending SMS
Implement the logic to send SMS messages using the Vonage SDK and your logger.
-
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
loggerfor initialization messages and fatal errors.
- Use the
-
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.*withlogger.*calls, providing context objects. - Handles missing
senderby returning a failure object instead of throwing, which is often better in batch processes. - Includes more contextual info (like
msgId,vonageStatus) in logs.
- Replaced all
3. Building the API Layer
Implement the /broadcast endpoint using your service and Promise.allSettled.
-
Import Service and Implement Endpoint (
index.js): Updateindex.jsto handle the/broadcastroute.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
sendSingleSmsandlogger. - Use
Promise.allSettledto 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.
- Integrate
-
Test the Endpoint: Restart your server (
npm run dev). Usecurlor 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.
4. Integrating with Vonage (Setup Recap & Details)
This section consolidates the Vonage-specific setup steps.
- Sign Up/Log In: Access the Vonage API Dashboard.
- Get API Key and Secret: Find these in the "API settings" section and add them to your
.envfile (VONAGE_API_KEY,VONAGE_API_SECRET). Keep the secret secure. - 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
.envasVONAGE_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.
- Test Numbers (Trial Accounts): Verify recipient numbers in the dashboard if using a trial account.
- Environment Variables Review: Ensure
.envhasVONAGE_API_KEY,VONAGE_API_SECRET,VONAGE_SENDER_ID,PORT,LOG_LEVEL.
5. Error Handling, Logging, and Retry Mechanisms
Production systems need robust handling.
-
Consistent Error Handling: The
sendSingleSmsfunction returns structured objects ({ success: boolean, ... }) even on failure, allowing the broadcast endpoint to aggregate results properly. The global error handler catches unexpected exceptions. -
Structured Logging: Integrate
pino(seelogger.js).- Logs are in JSON format (good for log aggregators).
pino-prettyused for readable development logs (npm run dev).- Log levels (
info,warn,error,fatal) are used appropriately. Contextual data is included in log objects.
- 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.allSettledin Section 3 is insufficient for production bulk sending. Throttle requests using proper concurrency control.- Recommended Library:
p-limit: Control concurrency.
bashnpm install p-limitExample using
p-limitin the/broadcasthandler (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.
- Recommended Library:
-
- Retry Mechanisms:
- Selective Retries: Only retry on transient errors (network issues, temporary Vonage
5xxerrors, rate limit errorsstatus: 1or HTTP 429). Avoid retrying permanent failures (invalid numberstatus: 3, auth errorsstatus: 4, unroutablestatus: 9). - Use Libraries:
async-retrycan help implement retries with exponential backoff. - Queue-Based Retries: Job queues (Section 9) usually have robust built-in retry features.
- Selective Retries: Only retry on transient errors (network issues, temporary Vonage
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) -->-
Technology Choice: PostgreSQL/MySQL with Prisma ORM recommended.
-
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.
-
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). -
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 ... }
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:
-
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
-
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
-
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)
-
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
-
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
Registration Process
- Navigate to Vonage Dashboard → "Brands and Campaigns"
- Complete Brand registration (include EIN/business registration)
- Wait for Brand verification (can take 1 – 5 business days)
- Register Campaign with detailed use case description
- Provide CTA documentation (hosted links to consent flows)
- Include opt-out instructions in sample messages
- Link registered Vonage numbers to approved campaign
- Wait for carrier approval (typically 1 – 2 weeks)
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.
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.
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.