code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / Vonage

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.

Build SMS Marketing Campaigns with Vonage Messages API, Node.js, and Express

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:

  1. Send individual or bulk SMS messages programmatically using the Vonage Messages API
  2. Receive incoming SMS messages sent to a dedicated Vonage number
  3. Receive delivery status updates for sent messages
  4. Provide API endpoints to trigger SMS sending
  5. 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:

mermaid
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

Prerequisites:

  • A Vonage API account. Sign up here.
  • 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.

  1. 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
  2. Initialize Node.js Project: This creates a package.json file to manage dependencies and project metadata.

    bash
    npm init -y
  3. 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.
  4. 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.
  5. Configure .gitignore: Add node_modules and .env to your .gitignore file to prevent committing them.

    text
    # .gitignore
    node_modules
    .env
  6. Set up Environment Variables (.env): Open the .env file and add the following placeholders. We will populate these values in the next section.

    dotenv
    # .env
    VONAGE_API_KEY=YOUR_VONAGE_API_KEY
    VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
    VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
    VONAGE_PRIVATE_KEY_PATH=./private.key
    VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
    
    PORT=3000
    • 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:

  1. Retrieve API Key and Secret:

    • Log in to your Vonage API Dashboard.
    • On the main dashboard page, you'll find your API key and API secret.
    • Copy these values and paste them into your .env file for VONAGE_API_KEY and VONAGE_API_SECRET.
  2. Set Default SMS API to Messages API:

    • Navigate to Account Settings in the dashboard.
    • Scroll down to the "API settings" 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.
  3. 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.
  4. 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.

  1. Initialize Express and Vonage SDK in server.js: Open server.js and add the following setup code:

    javascript
    // 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}`);
    });
    • 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).
  2. 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)
    
    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 };
      }
    }
    • 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 = 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 });
});
  • 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:

  1. Start the Server:

    bash
    node server.js

    You should see Server listening at http://localhost:3000.

  2. 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!"
             }'
    • You should receive a JSON response like:
      json
      {"status":"Message sending initiated","message_uuid":"<some-unique-id>"}
    • Shortly after, you should receive the SMS on your phone.
    • Check the terminal running server.js for log output confirming the message was sent.
  3. 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.

  1. 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: 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.
  2. 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:
      • Inbound URL: https://<YOUR_NGROK_SUBDOMAIN>.ngrok-free.app/webhooks/inbound
      • 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.
  3. Create Webhook Handler Routes in server.js: Add these routes before app.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.
  4. Restart the Server: Make sure ngrok is still running in its terminal. In the other terminal, start your server again:

    bash
    node server.js
  5. 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.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 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.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:

    • 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.