How to Build SMS Marketing Campaigns with Vonage Messages API and Node.js
Learn how to build SMS marketing campaigns using Vonage Messages API with Node.js and Express. Send bulk messages, implement webhooks, track delivery status, handle opt-outs, and ensure TCPA/GDPR compliance with complete code examples.
IMPORTANT NOTE: Despite the filename reference to "next-js-nextauth", this guide demonstrates SMS marketing campaigns using Node.js with Express framework. For Next.js-specific implementations, refer to Next.js API routes documentation. This tutorial focuses on the core Vonage Messages API integration patterns applicable to any Node.js environment.
Learn how to build a robust SMS marketing system by integrating the Vonage Messages API into your Node.js and Express application. This comprehensive guide walks you through sending targeted SMS messages, handling inbound responses, tracking delivery statuses, and ensuring compliance with telecommunications regulations including TCPA, GDPR, and CASL.
You'll create an Express application that sends SMS messages via API endpoints using the Vonage Messages API and receives inbound SMS messages plus delivery status updates through webhooks. This foundation enables you to build sophisticated SMS marketing campaign features with two-way communication, rate limiting, and opt-out handling using the official @vonage/server-sdk.
Project Overview and Goals
Goal: Create a Node.js application using the Express framework that can:
Send individual or bulk SMS messages programmatically using the Vonage Messages API
Receive incoming SMS messages sent to a dedicated Vonage number
Receive delivery status updates for sent messages
Provide API endpoints to trigger SMS sending
Handle compliance requirements for marketing campaigns
Problem Solved: Integrate reliable SMS functionality into applications for marketing, notifications, or communication purposes while handling both outbound and inbound messages effectively and maintaining regulatory compliance.
Technologies:
Node.js v22 (LTS): JavaScript runtime environment for building server-side applications (current LTS as of January 2025). Chosen for its asynchronous nature, large ecosystem (npm), and suitability for I/O-bound tasks like API interactions.
Express v4/v5: Minimal and flexible Node.js web application framework. Chosen for its simplicity in setting up web servers and defining API routes/webhooks.
Vonage Messages API: Unified API for sending and receiving messages across various channels, including SMS. Chosen for its comprehensive features, reliability, and developer-friendly SDK.
@vonage/server-sdk v3.25.1+: Official Vonage Node.js SDK (latest stable version released September 2024). Provides comprehensive API support for SMS, Voice, Verify, and other Vonage services.
dotenv: Module to load environment variables from a .env file into process.env. Essential for managing sensitive credentials securely.
ngrok: Tool to expose local servers to the internet. Crucial for testing webhooks during development.
System Architecture:
graph LR
subgraph Your Application
A[Express App] -->|Sends SMS via SDK| B(Vonage Node SDK);
A -->|Receives Webhooks| C{Webhook Endpoints (/inbound, /status)};
end
subgraph Internet
D(Developer/API Client) -->|POST /send-campaign| A;
E(User's Phone) -- SMS --> F[Vonage Platform];
F -- Webhook POST /inbound --> G(ngrok);
G -- Forwards Request --> C;
end
subgraph Vonage Cloud
B -->|API Call| F;
F -- SMS --> E;
F -- Webhook POST /status --> G;
end
style Your Application fill:#f9f,stroke:#333,stroke-width:2px
style Internet fill:#ccf,stroke:#333,stroke-width:2px
style Vonage Cloud fill:#cfc,stroke:#333,stroke-width:2px
Node.js v22 (LTS) and npm (or yarn) installed. Download Node.js. Verify installation: node --version should show v22.x.x.
A text editor (e.g., VS Code, Cursor, WebStorm).
A terminal or command prompt.
ngrok installed. Download ngrok. Authenticate ngrok (ngrok config add-authtoken YOUR_TOKEN) for higher limits and stable subdomains, which simplifies webhook testing during development.
(Optional but recommended) Vonage CLI for advanced operations: npm install -g @vonage/cli
How to Set Up Your Node.js SMS Marketing Project
Initialize your Node.js project and install the necessary dependencies for SMS marketing campaigns.
Create Project Directory:
Open your terminal and create a new directory for your project, then navigate into it.
Bash
mkdir vonage-sms-campaign
cd vonage-sms-campaign
Initialize Node.js Project:
This creates a package.json file to manage dependencies and project metadata.
Bash
npm init -y
Install Dependencies:
We need Express for the web server, the Vonage Server SDK to interact with the API, and dotenv for environment variable management.
Bash
npm install express @vonage/server-sdk dotenv
express: Web framework.
@vonage/server-sdk: Official Vonage SDK for Node.js.
dotenv: Loads environment variables from a .env file.
Create Project Structure:
Create the main application file and a file for environment variables.
Bash
touch server.js .env .gitignore
server.js: Our main Express application code.
.env: Stores sensitive credentials (API keys, etc.). Never commit this file to version control.
.gitignore: Specifies intentionally untracked files that Git should ignore.
Configure .gitignore:
Add node_modules and .env to your .gitignore file to prevent committing them.
Text
# .gitignore
node_modules
.env
Set up Environment Variables (.env):
Open the .env file and add the following placeholders. We will populate these values in the next section.
Under "Default SMS Setting", ensure Messages API is selected. If not, select it and click "Save changes". This ensures webhooks use the Messages API format.
Why? Vonage has older APIs (like the SMS API). Selecting Messages API ensures we use the modern, multi-channel API and receive webhooks in the expected format for the @vonage/server-sdk examples used here.
Create a Vonage Application:
Vonage Applications act as containers for your communication settings, including authentication (via keys) and webhook URLs.
Navigate to "Applications" in the dashboard menu, then click "Create a new application".
Name: Give your application a descriptive name (e.g., "Node SMS Campaign App").
Generate Public and Private Key: Click this button. Your browser will download a private.key file. Save this file securely in the root directory of your project (the same place as server.js). This key is used by the SDK to authenticate requests for this specific application. Update VONAGE_PRIVATE_KEY_PATH in your .env file if you save it elsewhere (e.g., config/private.key). Use ./private.key for consistency.
Capabilities: Toggle on the "Messages" capability.
Webhook URLs:
Inbound URL: Enter a placeholder for now, like http://example.com/webhooks/inbound. We will update this later with our ngrok URL. This is where Vonage sends incoming SMS messages.
Status URL: Enter a placeholder, like http://example.com/webhooks/status. We will update this later. This is where Vonage sends delivery status updates for outbound messages.
Click "Generate new application".
You'll be taken to the application details page. Copy the Application ID displayed.
Paste this ID into your .env file for VONAGE_APPLICATION_ID.
Link a Vonage Number:
You need a Vonage virtual number to send and receive messages.
If you don't have one, navigate to "Numbers" > "Buy numbers" in the dashboard and purchase an SMS-capable number.
Go back to your Application settings ("Applications" > Your App Name).
Scroll down to the "Linked numbers" section.
Click "Link" next to the virtual number you want to use with this application.
Copy the full phone number (including country code) and paste it into your .env file for VONAGE_NUMBER.
SMS Marketing Compliance: TCPA, GDPR, and CASL Requirements
CRITICAL: SMS marketing campaigns must comply with regional telecommunications regulations. Failure to comply can result in significant fines and legal penalties.
United States - TCPA Compliance:
Explicit Consent Required: Obtain written consent before sending marketing messages (effective since 1991, updated 2012-2013 for automated systems)
Opt-Out Mechanism: Every marketing message must include clear opt-out instructions (respond with "STOP")
Time Restrictions: Send messages only between 8 AM and 9 PM recipient local time
Penalties: $500-$1,500 per violation (can accumulate quickly with bulk campaigns)
Verification: Maintain detailed records of consent with timestamps and method
European Union - GDPR/ePrivacy:
Explicit Opt-In: Pre-checked boxes are non-compliant; require affirmative action
Purpose Limitation: Only use numbers for the stated purpose at time of collection
Right to Erasure: Implement mechanisms to delete user data upon request
Data Processing Records: Document lawful basis for processing (consent, legitimate interest, contract)
Canada - CASL (Canadian Anti-Spam Legislation):
Express Consent: Obtain explicit consent with clear identification and unsubscribe mechanism
Implied Consent: Limited to existing business relationships (expires after 24 months)
Penalties: Up to $10 million CAD per violation for businesses
Required Keywords (Industry Standard):
STOP, UNSTOP, UNSUBSCRIBE, END, QUIT: Must immediately cease marketing messages
HELP, INFO: Must provide customer service contact information
Response Time: Process opt-out requests within 24 hours (5 business days maximum for TCPA)
Best Practices:
Implement double opt-in confirmation for marketing lists
Store consent metadata: timestamp, IP address, form/method, specific language shown
Maintain suppression list synchronized across all sending systems
Include sender identification in every message (company name or brand)
Monitor complaint rates and delivery failures
Understanding Vonage API Rate Limits for Bulk SMS Campaigns
Messages API Rate Limits (as of 2025):
Default Throughput: 100 messages per second per account
Burst Capacity: Short bursts above limit may be accepted, but sustained rates will be throttled
Response: HTTP 429 (Too Many Requests) when rate limit exceeded
Retry-After Header: Indicates seconds to wait before retrying
Best Practices for High-Volume Campaigns:
Implement exponential backoff when receiving 429 responses
Use message queuing systems (Bull, BullMQ, AWS SQS) for large campaigns
Spread campaign sends over time rather than immediate bulk dispatch
Monitor API response codes and adjust sending rate dynamically
Consider Vonage's SMS Conversion API for very high volumes (contact sales)
Phone Number Format Requirements:
E.164 Format Required: International format with country code (e.g., 14155552671 for US)
No Special Characters: Remove spaces, dashes, parentheses, or plus symbols in API calls
Validation: Use libraries like libphonenumber-js to validate and format numbers
Example Formats:
US: 14155552671 (not +1-415-555-2671)
UK: 447700900123 (not +44 7700 900123)
Germany: 4915123456789 (not +49 151 234 56789)
How to Send SMS Messages Using Vonage Messages API
Write the code to send SMS messages using the Vonage Node.js SDK.
Initialize Express and Vonage SDK in server.js:
Open server.js and add the following setup code:
JavaScript
// server.jsrequire('dotenv').config(); // Load environment variables from .env fileconst express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const app = express();
const port = process.env.PORT || 3000;
// Middleware to parse JSON and URL-encoded bodies
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Initialize Vonage SDKlet vonage;
try {
vonage = newVonage({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH// Path to your private key file, e.g., ./private.key
}
// Optional: If privateKey is a string (e.g., from env var V_PRIV_KEY_STR) instead of a file path// Verify with current SDK docs if using this method:// , { auth: { privateKey: Buffer.from(process.env.VONAGE_PRIVATE_KEY_STRING, 'utf-8') } }
);
console.log("Vonage SDK Initialized successfully.");
} catch (error) {
console.error("Error initializing Vonage SDK:", error);
// Consider exiting if SDK initialization fails, as core functionality depends on it// process.exit(1);
}
// --- Routes will go here ---// Start the server
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
We load environment variables using dotenv.
We initialize Express and configure middleware to parse request bodies.
We initialize the Vonage SDK using the credentials and application details loaded from .env. A try...catch block handles potential errors during initialization (e.g., missing key file).
Create the Sending Function:
Add a function to handle the logic of sending an SMS message.
JavaScript
// server.js (add this function after Vonage initialization)asyncfunctionsendSms(toNumber, messageText) {
if (!vonage) {
console.error("Vonage SDK not initialized. Cannot send SMS.");
return { success: false, error: "Vonage SDK not available" };
}
const fromNumber = process.env.VONAGE_NUMBER;
try {
const resp = await vonage.messages.send({
message_type: "text",
text: messageText,
to: toNumber,
from: fromNumber,
channel: "sms"
});
console.log(`Message sent successfully to ${toNumber}. UUID: ${resp.message_uuid}`);
return { success: true, message_uuid: resp.message_uuid };
} catch (err) {
// Log more detailed error info if available from Vonage API responselet errorMessage = err.message;
let errorDetails = null;
if (err.response && err.response.data) {
errorMessage = `Vonage API Error: ${err.response.status} - ${JSON.stringify(err.response.data)}`;
errorDetails = err.response.data;
}
console.error(`Error sending SMS to ${toNumber}:`, errorMessage);
// Log the full error structure for deep debugging if needed// console.error("Full Vonage API Error Object:", JSON.stringify(err, null, 2));return { success: false, error: errorMessage, details: errorDetails };
}
}
This async function takes the recipient number and message text.
It uses vonage.messages.send with the required parameters:
message_type: "text": Specifies a plain text SMS.
text: The content of the message.
to: The recipient's phone number (E.164 format recommended, e.g., 14155552671).
from: Your Vonage virtual number.
channel: "sms": Explicitly specifies the SMS channel.
It uses async/await for cleaner handling of the promise returned by the SDK.
Error handling now attempts to log and return more detailed error information from err.response.data if it exists.
Building API Endpoints for SMS Campaign Management
Let's create a simple API endpoint to trigger sending SMS messages. This simulates how you might start a campaign or send individual messages from another part of your system or a frontend.
JavaScript
// server.js (add this route handler before app.listen)
app.post('/send-sms', async (req, res) => {
const { to, message } = req.body; // Expecting { "to": "RECIPIENT_NUMBER", "message": "Your text" }if (!to || !message) {
return res.status(400).json({ error: 'Missing required fields: "to" and "message"' });
}
// Basic validation (can be expanded)if (typeof to !== 'string' || typeof message !== 'string') {
return res.status(400).json({ error: 'Fields "to" and "message" must be strings' });
}
try {
const result = awaitsendSms(to, message);
if (result.success) {
res.status(200).json({ status: 'Message sending initiated', message_uuid: result.message_uuid });
} else {
// Use 500 for server-side/API errors during sending
res.status(500).json({ status: 'Failed to send message', error: result.error, details: result.details });
}
} catch (error) {
// Catch errors from the sendSms function itself (e.g., SDK not initialized)console.error("Internal server error in /send-sms:", error);
res.status(500).json({ status: 'Internal server error', error: error.message });
}
});
// Example: Endpoint for a very basic "campaign" (sending to multiple numbers)
app.post('/send-campaign', async (req, res) => {
const { recipients, message } = req.body; // Expecting { "recipients": ["NUM1", "NUM2"], "message": "Campaign text" }if (!Array.isArray(recipients) || recipients.length === 0 || !message) {
return res.status(400).json({ error: 'Requires "recipients" (array) and "message" (string)' });
}
const results = [];
// Send messages sequentially for simplicity here.// In production, consider parallel sending for better performance/throughput,// but be sure to implement proper rate limiting to avoid Vonage API limits.for (const recipient of recipients) {
if (typeof recipient === 'string') {
const result = awaitsendSms(recipient, message);
results.push({ to: recipient, ...result });
} else {
results.push({ to: recipient, success: false, error: "Invalid recipient format" });
}
}
// Check if any messages failed to initiate sendingconst hasFailures = results.some(r => !r.success);
const statusCode = hasFailures ? 207 : 200; // 207 Multi-Status if some failed
res.status(statusCode).json({ status: 'Campaign processing attempted', results });
});
We define a POST /send-sms route.
It expects a JSON body with to (recipient number) and message (text content).
Basic input validation checks for the presence and type of required fields.
It calls our sendSms function and returns a JSON response indicating success or failure, including details if available.
A second POST /send-campaign endpoint demonstrates sending the same message to multiple recipients provided in an array. Note: This simple loop sends messages sequentially. For high volume, parallel processing with rate limiting is advised for performance.
Testing the Sending API:
Start the Server:
Bash
node server.js
You should see Server listening at http://localhost:3000.
Send a Test Request (using curl):
Open a new terminal window. Replace YOUR_RECIPIENT_NUMBER with your actual phone number (e.g., 14155552671) and run:
Bash
curl -X POST http://localhost:3000/send-sms \
-H "Content-Type: application/json" \
-d '{
"to": "YOUR_RECIPIENT_NUMBER",
"message": "Hello from Vonage and Node.js!"
}'
Shortly after, you should receive the SMS on your phone.
Check the terminal running server.js for log output confirming the message was sent.
Test the Campaign Endpoint:
Bash
curl -X POST http://localhost:3000/send-campaign \
-H "Content-Type: application/json" \
-d '{
"recipients": ["YOUR_RECIPIENT_NUMBER_1", "YOUR_RECIPIENT_NUMBER_2"],
"message": "This is a test campaign message!"
}'
You should receive a response detailing the outcome for each recipient.
How to Receive Inbound SMS Messages with Vonage Webhooks
To receive incoming SMS messages and status updates, we need to expose our local server to the internet using ngrok and define webhook handler routes.
Start ngrok:
If your server is running, stop it (Ctrl+C). Make sure you are in your project directory in the terminal. Run ngrok, telling it to forward traffic to the port your Express app uses (default 3000). If you haven't already, consider authenticating ngrok (ngrok config add-authtoken YOUR_TOKEN) for stability.
Bash
ngrok http 3000
ngrok will display output similar to this:
Code
Session Status online
Account Your Name (Plan: Free/Paid)
Version x.x.x
Region United States (us-cal-1)
Forwarding https://<RANDOM_OR_STABLE_SUBDOMAIN>.ngrok-free.app -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Copy the https:// Forwarding URL. This is your public URL.
Update Vonage Application Webhook URLs:
Go back to your Vonage Application settings in the dashboard ("Applications" > Your App Name > Edit).
Paste your ngrok https:// URL into the Inbound URL and Status URL fields, appending the specific paths we will create:
Status URL:https://<YOUR_NGROK_SUBDOMAIN>.ngrok-free.app/webhooks/status
Click "Save changes".
Why separate paths? This helps organize the logic for handling different types of incoming webhook events.
Create Webhook Handler Routes in server.js:
Add these routes beforeapp.listen.
JavaScript
// server.js (add these route handlers)// Handles incoming SMS messages from users
app.post('/webhooks/inbound', (req, res) => {
console.log("--- Inbound SMS Received ---");
console.log("From:", req.body.from);
console.log("To:", req.body.to);
console.log("Text:", req.body.text);
console.log("Full Body:", JSON.stringify(req.body, null, 2)); // Log the full payload// Add your logic here:// - Store the message in a database// - Check for keywords (e.g., STOP, HELP)// - Trigger automated replies// Important: Respond with 200 OK quickly to acknowledge receipt
res.status(200).end();
});
// Handles delivery status updates for messages you sent
app.post('/webhooks/status', (req, res) => {
console.log("--- Message Status Update ---");
console.log("Message UUID:", req.body.message_uuid);
console.log("Status:", req.body.status);
console.log("Timestamp:", req.body.timestamp);
if(req.body.error) {
console.error("Error Code:", req.body.error.code);
console.error("Error Reason:", req.body.error.reason);
}
console.log("Full Body:", JSON.stringify(req.body, null, 2)); // Log the full payload// Add your logic here:// - Update message status in your database// - Trigger alerts on failures// - Analyze delivery rates// Important: Respond with 200 OK quickly
res.status(200).end();
});
We define POST handlers for /webhooks/inbound and /webhooks/status.
They log the relevant information from the request body (req.body). Vonage sends webhook data typically as application/json, which our express.json() middleware parses.
Crucially, both handlers immediately send a 200 OK response (res.status(200).end();). If Vonage doesn't receive a 200 OK quickly, it will assume the webhook failed and will retry, potentially leading to duplicate processing. Any time-consuming logic (database writes, external API calls) should be handled asynchronously after sending the response.
Restart the Server:
Make sure ngrok is still running in its terminal. In the other terminal, start your server again:
Bash
node server.js
Test Receiving:
Inbound: Send an SMS message from your personal phone to your Vonage virtual number (VONAGE_NUMBER). Check the terminal running server.js. You should see the "--- Inbound SMS Received ---" log entry with the message details.
Status: Send an SMS using the API (like you did with curl earlier). Check the server terminal again. You should see one or more "--- Message Status Update ---" logs for that message (e.g., submitted, delivered, or failed). The message_uuid will match the one returned by the API call.
Error Handling and Retry Logic for SMS Delivery
Production applications need robust error handling and logging.
Error Handling Strategy:
SDK Errors: Wrap vonage.messages.send calls in try...catch blocks. Inspect the err object provided by the SDK on failure (check err.response.data for Vonage-specific error details, as implemented in the updated sendSms function).
Webhook Errors: Implement try...catch within webhook handlers for your processing logic (database writes, etc.), but always ensure res.status(200).end() is sent reliably unless there's a fundamental issue with the request itself (e.g., malformed JSON, though Express handles this). Log errors encountered during processing.
Validation Errors: Return 4xx status codes for invalid client requests (e.g., missing parameters in API calls).
Server Errors: Return 5xx status codes for unexpected server-side issues.
Logging:
For development, console.log and console.error are sufficient.
For production, use a dedicated logging library like Winston or Pino for structured logging (JSON format), different log levels (info, warn, error), and routing logs to files or external services.
Example (Conceptual Logging with Winston):
Install Winston:
Bash
npm install winston
Add configuration to server.js:
JavaScript
// server.js (replace console.log/error)const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
// - Write all logs with level `error` and below to `error.log`// - Write all logs with level `info` and below to `combined.log`new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
// If we're not in production then log to the `console`if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}
// Usage example (replace console.log/error):// logger.info('Server started');// logger.error('Failed to send SMS', { error: err.message, details: err.response?.data });// Inside webhooks: logger.info('Inbound SMS received', { from: req.body.from });
Retry Mechanisms:
Vonage Webhooks: Vonage automatically retries webhooks if it doesn't receive a 200 OK. Ensure your handlers are idempotent (processing the same webhook multiple times doesn't cause negative side effects) or have logic to detect duplicates if necessary (e.g., checking message_uuid against recently processed messages).
Outbound API Calls: If your sendSms call fails due to a potentially temporary issue (e.g., network error, Vonage 5xx error), you might implement a retry strategy with exponential backoff. Libraries like async-retry can simplify this.
Example (Conceptual Retry with async-retry):
Install async-retry:
Bash
npm install async-retry
Update server.js to include a retry function:
JavaScript
// server.jsconst retry = require('async-retry');
// Make sure 'logger' is defined (e.g., using Winston as above, or fallback to console)const logger = console; // Replace with your actual logger instanceasyncfunctionsendSmsWithRetry(toNumber, messageText) {
try {
returnawaitretry(async bail => {
// bail is a function to prevent further retries if the error is permanentif (!vonage) {
logger.error("Vonage SDK not initialized.");
bail(newError("Vonage SDK not available")); // Don't retry if SDK isn't set upreturn; // Explicitly return undefined or throw
}
const fromNumber = process.env.VONAGE_NUMBER;
try {
const resp = await vonage.messages.send({
message_type: "text",
text: messageText,
to: toNumber,
from: fromNumber,
channel: "sms"
});
logger.info(`Message sent successfully to ${toNumber}. UUID: ${resp.message_uuid}`);
return { success: true, message_uuid: resp.message_uuid };
} catch (err) {
logger.warn(`Attempt failed sending SMS to ${toNumber}:`, err.message);
// Example: Don't retry on client errors (4xx) like invalid numberif (err.response && err.response.status >= 400 && err.response.status < 500) {
const errorDetails = err.response.data || { code: err.response.status, reason: 'Client Error' };
logger.error(`Permanent error sending SMS to ${toNumber}. Not retrying.`, errorDetails);
// Bail and return a structured errorbail(newError(`Vonage API client error: ${err.response.status}`));
// Return statement below might not be reached if bail throws, but included for clarityreturn { success: false, error: `Vonage API client error: ${err.response.status}`, details: errorDetails };
}
// For other errors (network, 5xx), throw to trigger retrythrow err;
}
}, {
retries: 3, // Number of retriesfactor: 2, // Exponential backoff factorminTimeout: 1000, // Initial timeout in msonRetry: (error, attempt) => {
logger.warn(`Retrying SMS send to ${toNumber}. Attempt ${attempt}. Error: ${error.message}`);
}
});
} catch (error) {
// Catch errors even after retries or if bail was called
logger.error(`Failed to send SMS to ${toNumber} after retries:`, error.message);
// Try to extract details if it was a Vonage error that caused the final failureconst finalDetails = error.response?.data || null;
return { success: false, error: error.message, details: finalDetails };
}
}
// Remember to replace calls to sendSms with sendSmsWithRetry in your API endpoints
Essential Security Best Practices for SMS Marketing APIs
Secure Credential Management: Use environment variables (.env file loaded by dotenv) and ensure .env is in your .gitignore. Never hardcode credentials in your source code. In production, use platform-specific secret management (e.g., Heroku Config Vars, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault).
Input Validation: Sanitize and validate all input coming from users or external APIs.
In the /send-sms and /send-campaign endpoints, we added basic checks. Use libraries like express-validator for more complex validation rules.
For phone numbers, validate the format (e.g., E.164). Libraries like libphonenumber-js can help.
Limit message length.
Rate Limiting: Protect your API endpoints (especially /send-sms, /send-campaign) from abuse. Use middleware like express-rate-limit.
Install express-rate-limit:
Bash
npm install express-rate-limit
Apply the middleware in server.js:
JavaScript
// server.jsconst rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 100, // Limit each IP to 100 requests per windowMsstandardHeaders: true, // Return rate limit info in the `RateLimit-*` headerslegacyHeaders: false, // Disable the `X-RateLimit-*` headersmessage: 'Too many requests from this IP, please try again after 15 minutes'
});
// Apply to specific API routes
app.use('/send-sms', apiLimiter);
app.use('/send-campaign', apiLimiter);
// ... rest of your routes ...
Webhook Security: Vonage does not currently offer signed webhooks for the Messages API like it does for some other APIs (e.g., Voice). Therefore, the primary security mechanism provided by the platform is the obscurity of your webhook endpoint URLs. Ensure these URLs are complex and not easily guessable. As additional layers you can implement (though not standard Vonage features for Messages API), consider:
IP Address Whitelisting: If Vonage provides a stable list of egress IP addresses for webhooks (check their documentation), you could configure your firewall or application to only accept requests from those IPs.
Shared Secret: Include a hard-to-guess secret token as a query parameter in the webhook URL you configure in Vonage (e.g., .../inbound?token=YOUR_SECRET). Your application then verifies this token on every incoming request. This requires careful secret management.
Protect Against Common Vulnerabilities: While less critical for a simple backend SMS API than a full web app, be aware of OWASP Top 10 vulnerabilities (e.g., Injection flaws if constructing dynamic replies based on input, though less common with direct SMS). Use security linters and scanners in your CI/CD pipeline.
Opt-Out Handling: For marketing messages, comply with regulations (like TCPA in the US). Implement logic in your /webhooks/inbound handler to detect STOP, UNSUBSCRIBE, etc., keywords and add the sender's number (req.body.from) to a suppression list to prevent sending further messages.
Handling Character Limits, Encoding, and Delivery Receipts
Character Limits & Encoding: Standard SMS messages are 160 characters (GSM-7 encoding). Using characters outside this set (like many emojis) switches to Unicode (UCS-2) encoding, limiting messages to 70 characters. Longer messages are split into multiple segments (concatenated SMS). Vonage handles segmentation, but each segment is billed. Be mindful of message length to control costs. Inform users if their input will exceed limits or result in higher costs.
Delivery Receipts (DLRs): The /webhooks/status endpoint receives DLRs. Use the status field (e.g., delivered, failed, rejected) and message_uuid to track the final outcome of your sent messages. Analyze failure reasons (error.code, error.reason) to troubleshoot issues.
Frequently asked questions
Use the Vonage Messages API and Server SDK. Initialize the SDK with your API credentials, then use vonage.messages.send() to send text messages. Make sure to handle the asynchronous nature of the API call using async/await and implement error handling.
The Vonage Messages API is a unified platform for sending and receiving messages across various channels, including SMS. It provides a reliable and developer-friendly way to integrate messaging functionality into applications.
The Messages API is Vonage's modern, multi-channel API offering more features and a consistent webhook format. Using it ensures compatibility with the latest Vonage SDKs and best practices.
Use ngrok during development to expose your local server to the internet so Vonage can reach your webhook endpoints. This is essential for testing inbound message and delivery status updates.
Yes, the provided /send-campaign example demonstrates basic bulk sending. For high-volume campaigns, implement parallel sending with appropriate rate limiting for optimal performance and to avoid exceeding Vonage API limits.
Set up webhook endpoints in your Express app (e.g., /webhooks/inbound) and configure these URLs in your Vonage application settings. Vonage will send incoming SMS data to your specified endpoints.
The private.key file is used by the Vonage SDK for authentication and authorization when making API requests. It's essential for securing your Vonage integration and preventing unauthorized access.
Create a webhook endpoint (e.g., /webhooks/status) to receive delivery receipts (DLRs). Vonage will send updates to this endpoint, providing information about message status (delivered, failed, etc.).
Vonage Applications act as containers for your communication settings and webhook configurations. They manage authentication via key pairs and centralize your Vonage API integration details.
Be aware that standard SMS messages have a 160-character limit (GSM-7 encoding) or 70 characters for Unicode (UCS-2). Longer messages are split into segments, which are billed individually. Inform users about potential extra costs or limit input length.
Vonage uses the 200 OK response to acknowledge webhook delivery. Without it, Vonage assumes failure and will retry the webhook, potentially leading to duplicate processing. Always send 200 OK promptly, then handle time-consuming processing asynchronously.
Use environment variables for credentials, validate all user input, implement rate limiting, and consider IP whitelisting or shared secrets for webhooks. Securely store the private.key file and never commit it to version control.
Common errors include incorrect API credentials, invalid webhook URLs, failure to respond to webhooks with 200 OK, exceeding character limits, and not handling delivery status updates properly.
Use try...catch blocks for SDK calls and webhook handlers. Inspect the err object from the SDK, log errors, and use retry mechanisms (e.g., with async-retry) with exponential backoff for transient failures.
Use a dedicated logging library like Winston or Pino for structured logging, different log levels, and routing logs to appropriate destinations like files or external services.
IMPORTANT NOTE: Despite the filename reference to "next-js-nextauth", this guide demonstrates SMS marketing campaigns using Node.js with Express framework. For Next.js-specific implementations, refer to Next.js API routes documentation. This tutorial focuses on the core Vonage Messages API integration patterns applicable to any Node.js environment.
Learn how to build a robust SMS marketing system by integrating the Vonage Messages API into your Node.js and Express application. This comprehensive guide walks you through sending targeted SMS messages, handling inbound responses, tracking delivery statuses, and ensuring compliance with telecommunications regulations including TCPA, GDPR, and CASL.
You'll create an Express application that sends SMS messages via API endpoints using the Vonage Messages API and receives inbound SMS messages plus delivery status updates through webhooks. This foundation enables you to build sophisticated SMS marketing campaign features with two-way communication, rate limiting, and opt-out handling using the official @vonage/server-sdk.
Project Overview and Goals
Goal: Create a Node.js application using the Express framework that can:
Problem Solved: Integrate reliable SMS functionality into applications for marketing, notifications, or communication purposes while handling both outbound and inbound messages effectively and maintaining regulatory compliance.
Technologies:
.envfile intoprocess.env. Essential for managing sensitive credentials securely.System Architecture:
Prerequisites:
node --versionshould show v22.x.x.ngrok config add-authtoken YOUR_TOKEN) for higher limits and stable subdomains, which simplifies webhook testing during development.npm install -g @vonage/cliHow to Set Up Your Node.js SMS Marketing Project
Initialize your Node.js project and install the necessary dependencies for SMS marketing campaigns.
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
mkdir vonage-sms-campaign cd vonage-sms-campaignInitialize Node.js Project: This creates a
package.jsonfile to manage dependencies and project metadata.Install Dependencies: We need Express for the web server, the Vonage Server SDK to interact with the API, and dotenv for environment variable management.
express: Web framework.@vonage/server-sdk: Official Vonage SDK for Node.js.dotenv: Loads environment variables from a.envfile.Create Project Structure: Create the main application file and a file for environment variables.
touch server.js .env .gitignoreserver.js: Our main Express application code..env: Stores sensitive credentials (API keys, etc.). Never commit this file to version control..gitignore: Specifies intentionally untracked files that Git should ignore.Configure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing them.Set up Environment Variables (
.env): Open the.envfile and add the following placeholders. We will populate these values in the next section.VONAGE_API_KEY,VONAGE_API_SECRET: Your Vonage account credentials.VONAGE_APPLICATION_ID: ID of the Vonage application we'll create.VONAGE_PRIVATE_KEY_PATH: Path to the private key file for the Vonage application.VONAGE_NUMBER: The Vonage virtual number you'll use for sending/receiving SMS.PORT: The port your Express server will listen on.How to Configure Your Vonage Account for SMS Marketing
Before writing code, we need to configure our Vonage account and application. Here's how to set up Vonage for SMS campaigns:
Retrieve API Key and Secret:
API keyandAPI secret..envfile forVONAGE_API_KEYandVONAGE_API_SECRET.Set Default SMS API to
Messages API:Messages APIis selected. If not, select it and click "Save changes". This ensures webhooks use the Messages API format.Messages APIensures we use the modern, multi-channel API and receive webhooks in the expected format for the@vonage/server-sdkexamples used here.Create a Vonage Application: Vonage Applications act as containers for your communication settings, including authentication (via keys) and webhook URLs.
private.keyfile. Save this file securely in the root directory of your project (the same place asserver.js). This key is used by the SDK to authenticate requests for this specific application. UpdateVONAGE_PRIVATE_KEY_PATHin your.envfile if you save it elsewhere (e.g.,config/private.key). Use./private.keyfor consistency.http://example.com/webhooks/inbound. We will update this later with our ngrok URL. This is where Vonage sends incoming SMS messages.http://example.com/webhooks/status. We will update this later. This is where Vonage sends delivery status updates for outbound messages..envfile forVONAGE_APPLICATION_ID.Link a Vonage Number: You need a Vonage virtual number to send and receive messages.
.envfile forVONAGE_NUMBER.SMS Marketing Compliance: TCPA, GDPR, and CASL Requirements
CRITICAL: SMS marketing campaigns must comply with regional telecommunications regulations. Failure to comply can result in significant fines and legal penalties.
United States - TCPA Compliance:
European Union - GDPR/ePrivacy:
Canada - CASL (Canadian Anti-Spam Legislation):
Required Keywords (Industry Standard):
Best Practices:
Understanding Vonage API Rate Limits for Bulk SMS Campaigns
Messages API Rate Limits (as of 2025):
Best Practices for High-Volume Campaigns:
Phone Number Format Requirements:
14155552671for US)libphonenumber-jsto validate and format numbers14155552671(not+1-415-555-2671)447700900123(not+44 7700 900123)4915123456789(not+49 151 234 56789)How to Send SMS Messages Using Vonage Messages API
Write the code to send SMS messages using the Vonage Node.js SDK.
Initialize Express and Vonage SDK in
server.js: Openserver.jsand add the following setup code:// server.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); const app = express(); const port = process.env.PORT || 3000; // Middleware to parse JSON and URL-encoded bodies app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Initialize Vonage SDK let vonage; try { vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: process.env.VONAGE_PRIVATE_KEY_PATH // Path to your private key file, e.g., ./private.key } // Optional: If privateKey is a string (e.g., from env var V_PRIV_KEY_STR) instead of a file path // Verify with current SDK docs if using this method: // , { auth: { privateKey: Buffer.from(process.env.VONAGE_PRIVATE_KEY_STRING, 'utf-8') } } ); console.log("Vonage SDK Initialized successfully."); } catch (error) { console.error("Error initializing Vonage SDK:", error); // Consider exiting if SDK initialization fails, as core functionality depends on it // process.exit(1); } // --- Routes will go here --- // Start the server app.listen(port, () => { console.log(`Server listening at http://localhost:${port}`); });dotenv..env. Atry...catchblock handles potential errors during initialization (e.g., missing key file).Create the Sending Function: Add a function to handle the logic of sending an SMS message.
// server.js (add this function after Vonage initialization) async function sendSms(toNumber, messageText) { if (!vonage) { console.error("Vonage SDK not initialized. Cannot send SMS."); return { success: false, error: "Vonage SDK not available" }; } const fromNumber = process.env.VONAGE_NUMBER; try { const resp = await vonage.messages.send({ message_type: "text", text: messageText, to: toNumber, from: fromNumber, channel: "sms" }); console.log(`Message sent successfully to ${toNumber}. UUID: ${resp.message_uuid}`); return { success: true, message_uuid: resp.message_uuid }; } catch (err) { // Log more detailed error info if available from Vonage API response let errorMessage = err.message; let errorDetails = null; if (err.response && err.response.data) { errorMessage = `Vonage API Error: ${err.response.status} - ${JSON.stringify(err.response.data)}`; errorDetails = err.response.data; } console.error(`Error sending SMS to ${toNumber}:`, errorMessage); // Log the full error structure for deep debugging if needed // console.error("Full Vonage API Error Object:", JSON.stringify(err, null, 2)); return { success: false, error: errorMessage, details: errorDetails }; } }asyncfunction takes the recipient number and message text.vonage.messages.sendwith the required parameters:message_type: "text": Specifies a plain text SMS.text: The content of the message.to: The recipient's phone number (E.164 format recommended, e.g.,14155552671).from: Your Vonage virtual number.channel: "sms": Explicitly specifies the SMS channel.async/awaitfor cleaner handling of the promise returned by the SDK.err.response.dataif it exists.Building API Endpoints for SMS Campaign Management
Let's create a simple API endpoint to trigger sending SMS messages. This simulates how you might start a campaign or send individual messages from another part of your system or a frontend.
// server.js (add this route handler before app.listen) app.post('/send-sms', async (req, res) => { const { to, message } = req.body; // Expecting { "to": "RECIPIENT_NUMBER", "message": "Your text" } if (!to || !message) { return res.status(400).json({ error: 'Missing required fields: "to" and "message"' }); } // Basic validation (can be expanded) if (typeof to !== 'string' || typeof message !== 'string') { return res.status(400).json({ error: 'Fields "to" and "message" must be strings' }); } try { const result = await sendSms(to, message); if (result.success) { res.status(200).json({ status: 'Message sending initiated', message_uuid: result.message_uuid }); } else { // Use 500 for server-side/API errors during sending res.status(500).json({ status: 'Failed to send message', error: result.error, details: result.details }); } } catch (error) { // Catch errors from the sendSms function itself (e.g., SDK not initialized) console.error("Internal server error in /send-sms:", error); res.status(500).json({ status: 'Internal server error', error: error.message }); } }); // Example: Endpoint for a very basic "campaign" (sending to multiple numbers) app.post('/send-campaign', async (req, res) => { const { recipients, message } = req.body; // Expecting { "recipients": ["NUM1", "NUM2"], "message": "Campaign text" } if (!Array.isArray(recipients) || recipients.length === 0 || !message) { return res.status(400).json({ error: 'Requires "recipients" (array) and "message" (string)' }); } const results = []; // Send messages sequentially for simplicity here. // In production, consider parallel sending for better performance/throughput, // but be sure to implement proper rate limiting to avoid Vonage API limits. for (const recipient of recipients) { if (typeof recipient === 'string') { const result = await sendSms(recipient, message); results.push({ to: recipient, ...result }); } else { results.push({ to: recipient, success: false, error: "Invalid recipient format" }); } } // Check if any messages failed to initiate sending const hasFailures = results.some(r => !r.success); const statusCode = hasFailures ? 207 : 200; // 207 Multi-Status if some failed res.status(statusCode).json({ status: 'Campaign processing attempted', results }); });POST /send-smsroute.to(recipient number) andmessage(text content).sendSmsfunction and returns a JSON response indicating success or failure, including details if available.POST /send-campaignendpoint demonstrates sending the same message to multiple recipients provided in an array. Note: This simple loop sends messages sequentially. For high volume, parallel processing with rate limiting is advised for performance.Testing the Sending API:
Start the Server:
You should see
Server listening at http://localhost:3000.Send a Test Request (using
curl): Open a new terminal window. ReplaceYOUR_RECIPIENT_NUMBERwith your actual phone number (e.g.,14155552671) and run:curl -X POST http://localhost:3000/send-sms \ -H "Content-Type: application/json" \ -d '{ "to": "YOUR_RECIPIENT_NUMBER", "message": "Hello from Vonage and Node.js!" }'{"status":"Message sending initiated","message_uuid":"<some-unique-id>"}server.jsfor log output confirming the message was sent.Test the Campaign Endpoint:
curl -X POST http://localhost:3000/send-campaign \ -H "Content-Type: application/json" \ -d '{ "recipients": ["YOUR_RECIPIENT_NUMBER_1", "YOUR_RECIPIENT_NUMBER_2"], "message": "This is a test campaign message!" }'How to Receive Inbound SMS Messages with Vonage Webhooks
To receive incoming SMS messages and status updates, we need to expose our local server to the internet using ngrok and define webhook handler routes.
Start ngrok: If your server is running, stop it (Ctrl+C). Make sure you are in your project directory in the terminal. Run ngrok, telling it to forward traffic to the port your Express app uses (default 3000). If you haven't already, consider authenticating ngrok (
ngrok config add-authtoken YOUR_TOKEN) for stability.https://Forwarding URL. This is your public URL.Update Vonage Application Webhook URLs:
https://URL into the Inbound URL and Status URL fields, appending the specific paths we will create:https://<YOUR_NGROK_SUBDOMAIN>.ngrok-free.app/webhooks/inboundhttps://<YOUR_NGROK_SUBDOMAIN>.ngrok-free.app/webhooks/statusCreate Webhook Handler Routes in
server.js: Add these routes beforeapp.listen.// server.js (add these route handlers) // Handles incoming SMS messages from users app.post('/webhooks/inbound', (req, res) => { console.log("--- Inbound SMS Received ---"); console.log("From:", req.body.from); console.log("To:", req.body.to); console.log("Text:", req.body.text); console.log("Full Body:", JSON.stringify(req.body, null, 2)); // Log the full payload // Add your logic here: // - Store the message in a database // - Check for keywords (e.g., STOP, HELP) // - Trigger automated replies // Important: Respond with 200 OK quickly to acknowledge receipt res.status(200).end(); }); // Handles delivery status updates for messages you sent app.post('/webhooks/status', (req, res) => { console.log("--- Message Status Update ---"); console.log("Message UUID:", req.body.message_uuid); console.log("Status:", req.body.status); console.log("Timestamp:", req.body.timestamp); if(req.body.error) { console.error("Error Code:", req.body.error.code); console.error("Error Reason:", req.body.error.reason); } console.log("Full Body:", JSON.stringify(req.body, null, 2)); // Log the full payload // Add your logic here: // - Update message status in your database // - Trigger alerts on failures // - Analyze delivery rates // Important: Respond with 200 OK quickly res.status(200).end(); });POSThandlers for/webhooks/inboundand/webhooks/status.req.body). Vonage sends webhook data typically asapplication/json, which ourexpress.json()middleware parses.200 OKresponse (res.status(200).end();). If Vonage doesn't receive a 200 OK quickly, it will assume the webhook failed and will retry, potentially leading to duplicate processing. Any time-consuming logic (database writes, external API calls) should be handled asynchronously after sending the response.Restart the Server: Make sure ngrok is still running in its terminal. In the other terminal, start your server again:
Test Receiving:
VONAGE_NUMBER). Check the terminal runningserver.js. You should see the "--- Inbound SMS Received ---" log entry with the message details.curlearlier). Check the server terminal again. You should see one or more "--- Message Status Update ---" logs for that message (e.g.,submitted,delivered, orfailed). Themessage_uuidwill match the one returned by the API call.Error Handling and Retry Logic for SMS Delivery
Production applications need robust error handling and logging.
Error Handling Strategy:
vonage.messages.sendcalls intry...catchblocks. Inspect theerrobject provided by the SDK on failure (checkerr.response.datafor Vonage-specific error details, as implemented in the updatedsendSmsfunction).try...catchwithin webhook handlers for your processing logic (database writes, etc.), but always ensureres.status(200).end()is sent reliably unless there's a fundamental issue with the request itself (e.g., malformed JSON, though Express handles this). Log errors encountered during processing.4xxstatus codes for invalid client requests (e.g., missing parameters in API calls).5xxstatus codes for unexpected server-side issues.Logging:
console.logandconsole.errorare sufficient.Example (Conceptual Logging with Winston):
Install Winston:
Add configuration to
server.js:// server.js (replace console.log/error) const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ // - Write all logs with level `error` and below to `error.log` // - Write all logs with level `info` and below to `combined.log` new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), ], }); // If we're not in production then log to the `console` if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.simple(), })); } // Usage example (replace console.log/error): // logger.info('Server started'); // logger.error('Failed to send SMS', { error: err.message, details: err.response?.data }); // Inside webhooks: logger.info('Inbound SMS received', { from: req.body.from });Retry Mechanisms:
200 OK. Ensure your handlers are idempotent (processing the same webhook multiple times doesn't cause negative side effects) or have logic to detect duplicates if necessary (e.g., checkingmessage_uuidagainst recently processed messages).sendSmscall fails due to a potentially temporary issue (e.g., network error, Vonage 5xx error), you might implement a retry strategy with exponential backoff. Libraries likeasync-retrycan simplify this.Example (Conceptual Retry with
async-retry):Install
async-retry:Update
server.jsto include a retry function:// server.js const retry = require('async-retry'); // Make sure 'logger' is defined (e.g., using Winston as above, or fallback to console) const logger = console; // Replace with your actual logger instance async function sendSmsWithRetry(toNumber, messageText) { try { return await retry(async bail => { // bail is a function to prevent further retries if the error is permanent if (!vonage) { logger.error("Vonage SDK not initialized."); bail(new Error("Vonage SDK not available")); // Don't retry if SDK isn't set up return; // Explicitly return undefined or throw } const fromNumber = process.env.VONAGE_NUMBER; try { const resp = await vonage.messages.send({ message_type: "text", text: messageText, to: toNumber, from: fromNumber, channel: "sms" }); logger.info(`Message sent successfully to ${toNumber}. UUID: ${resp.message_uuid}`); return { success: true, message_uuid: resp.message_uuid }; } catch (err) { logger.warn(`Attempt failed sending SMS to ${toNumber}:`, err.message); // Example: Don't retry on client errors (4xx) like invalid number if (err.response && err.response.status >= 400 && err.response.status < 500) { const errorDetails = err.response.data || { code: err.response.status, reason: 'Client Error' }; logger.error(`Permanent error sending SMS to ${toNumber}. Not retrying.`, errorDetails); // Bail and return a structured error bail(new Error(`Vonage API client error: ${err.response.status}`)); // Return statement below might not be reached if bail throws, but included for clarity return { success: false, error: `Vonage API client error: ${err.response.status}`, details: errorDetails }; } // For other errors (network, 5xx), throw to trigger retry throw err; } }, { retries: 3, // Number of retries factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial timeout in ms onRetry: (error, attempt) => { logger.warn(`Retrying SMS send to ${toNumber}. Attempt ${attempt}. Error: ${error.message}`); } }); } catch (error) { // Catch errors even after retries or if bail was called logger.error(`Failed to send SMS to ${toNumber} after retries:`, error.message); // Try to extract details if it was a Vonage error that caused the final failure const finalDetails = error.response?.data || null; return { success: false, error: error.message, details: finalDetails }; } } // Remember to replace calls to sendSms with sendSmsWithRetry in your API endpointsEssential Security Best Practices for SMS Marketing APIs
Secure Credential Management: Use environment variables (
.envfile loaded bydotenv) and ensure.envis in your.gitignore. Never hardcode credentials in your source code. In production, use platform-specific secret management (e.g., Heroku Config Vars, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault).Input Validation: Sanitize and validate all input coming from users or external APIs.
/send-smsand/send-campaignendpoints, we added basic checks. Use libraries likeexpress-validatorfor more complex validation rules.libphonenumber-jscan help.Rate Limiting: Protect your API endpoints (especially
/send-sms,/send-campaign) from abuse. Use middleware likeexpress-rate-limit.Install
express-rate-limit:Apply the middleware in
server.js:// server.js const rateLimit = require('express-rate-limit'); const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers message: 'Too many requests from this IP, please try again after 15 minutes' }); // Apply to specific API routes app.use('/send-sms', apiLimiter); app.use('/send-campaign', apiLimiter); // ... rest of your routes ...Webhook Security: Vonage does not currently offer signed webhooks for the Messages API like it does for some other APIs (e.g., Voice). Therefore, the primary security mechanism provided by the platform is the obscurity of your webhook endpoint URLs. Ensure these URLs are complex and not easily guessable. As additional layers you can implement (though not standard Vonage features for Messages API), consider:
.../inbound?token=YOUR_SECRET). Your application then verifies this token on every incoming request. This requires careful secret management.Protect Against Common Vulnerabilities: While less critical for a simple backend SMS API than a full web app, be aware of OWASP Top 10 vulnerabilities (e.g., Injection flaws if constructing dynamic replies based on input, though less common with direct SMS). Use security linters and scanners in your CI/CD pipeline.
Opt-Out Handling: For marketing messages, comply with regulations (like TCPA in the US). Implement logic in your
/webhooks/inboundhandler to detect STOP, UNSUBSCRIBE, etc., keywords and add the sender's number (req.body.from) to a suppression list to prevent sending further messages.Handling Character Limits, Encoding, and Delivery Receipts
/webhooks/statusendpoint receives DLRs. Use thestatusfield (e.g.,delivered,failed,rejected) andmessage_uuidto track the final outcome of your sent messages. Analyze failure reasons (error.code,error.reason) to troubleshoot issues.