code examples

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

Vonage SMS API Node.js Tutorial: Build Marketing Campaigns with Express (2025 Guide)

Learn how to build SMS marketing campaigns with Vonage Messages API, Node.js, and Express. Complete tutorial with webhooks, TCPA compliance, rate limiting, bulk messaging, and production deployment best practices.

Vonage SMS Node.js Tutorial: Send & Receive Messages with Express API (2025)

Learn how to build SMS marketing campaigns with Vonage Messages API, Node.js, and Express. This comprehensive 2025 tutorial teaches you to send bulk SMS messages, handle webhooks for two-way messaging, implement TCPA-compliant opt-out handling, and deploy production-ready SMS platforms.

By completing this Vonage SMS API tutorial, you'll build a functional marketing messaging platform that:

  1. Sends SMS messages programmatically via the Vonage Messages API
  2. Receives incoming SMS messages through webhooks
  3. Logs message statuses and tracks delivery
  4. Implements TCPA-compliant opt-out handling (required for marketing campaigns)
  5. Respects API rate limits (30 requests/second)

This solution addresses common needs for SMS notifications, alerts, two-way communication, and marketing campaigns while maintaining compliance with US regulations.

Frequently Asked Questions

How do I send SMS messages with Vonage API in Node.js?

Install the @vonage/server-sdk package, initialize the Vonage client with your API credentials and application ID, then use vonage.messages.send() with channel set to 'sms'. The complete code example in Section 3 shows the full implementation including authentication with private keys and error handling.

What's the difference between Vonage SMS API and Messages API?

The Messages API is the newer, unified API supporting SMS, MMS, WhatsApp, and other channels. It uses application-based authentication with private keys and provides richer webhook data. The older SMS API uses API key/secret authentication only. Set your default to Messages API in the Vonage Dashboard under Account → API settings.

How much does Vonage SMS pricing cost per message in the US?

Vonage charges $0.0075 per outbound SMS and $0.0069 per inbound SMS in the US (as of 2025). A complete two-way conversation costs approximately $0.0144. Volume discounts available through account managers for high-volume senders. Check the official Vonage pricing page for current rates.

What are Vonage API rate limits for SMS?

Vonage defaults to 30 requests per second (2,592,000 SMS per day). The Messages API sandbox limits to 1 message per second and 100 messages per month. Exceeding limits returns HTTP 429 or status code 1 (Throttled). Implement rate limiting with libraries like p-limit as shown in Section 8.

How do I set up SMS webhooks with Vonage Messages API in Node.js?

Create an Express server with POST endpoints at /webhooks/inbound (for incoming messages) and /webhooks/status (for delivery status). Expose your local server with ngrok or Cloudflare Tunnel, then configure these URLs in your Vonage Application settings. Always return HTTP 200 to acknowledge webhook receipt. See Section 4 for complete implementation.

Is TCPA compliance required for Vonage SMS marketing campaigns?

Yes, if you're sending marketing messages to US recipients. The FCC's April 11, 2025 Opt-Out Rule requires honoring opt-outs within 10 business days and recognizing diverse opt-out language beyond "STOP" (like "Leave me alone" or "Don't text me"). Penalties range from $500-$1,500 per violation. Section 7 provides full compliance requirements and implementation code.

What's better for Vonage webhooks: ngrok or Cloudflare Tunnel?

For development, both work. ngrok offers quicker setup but free tier limits to 1 GB/month bandwidth with URL changes on restart. Cloudflare Tunnel provides unlimited bandwidth, free for up to 50 users, built-in DDoS protection, and more stable URLs. For production, use neither – deploy to a hosting platform with a persistent domain. See Section 5 for detailed comparison and setup instructions.

How do I implement opt-out handling for SMS marketing with Vonage?

Detect opt-out keywords in your /webhooks/inbound handler (STOP, UNSUBSCRIBE, "leave me alone," etc.), store the opt-out in your database with timestamp, remove the number from marketing lists, and send a confirmation message within 5 minutes. Maintain records for 4 years per TCPA requirements. Section 7 includes a complete code example.

How do I send bulk SMS messages with Vonage API without rate limiting errors?

Yes, use the p-limit library to throttle concurrent requests to 30 per second (matching Vonage's default limit). The bulk-send implementation in Section 8 shows how to send to multiple recipients while respecting rate limits and handling individual message errors gracefully.

What Node.js version works with Vonage Messages API?

Vonage recommends Node.js LTS (Long Term Support) versions. As of 2025, @vonage/server-sdk@3.24.1 supports Node.js 14+, with Node.js 18 LTS or 20 LTS recommended for production deployments. Check the official Vonage SDK documentation for current compatibility matrix.

What You'll Build: SMS Marketing Platform with Vonage API

What You're Building: A Node.js application using the Express framework with two main functions:

  1. A script to send SMS messages using the Vonage Messages API
  2. An Express server to receive incoming SMS messages via webhooks

Problem Solved: Enables programmatic SMS communication for automated messages, user replies, inbound messages, and marketing campaigns with regulatory compliance.

Technologies Used:

  • Node.js: JavaScript runtime for server-side applications. Chosen for asynchronous operations, npm ecosystem, and I/O-bound tasks like API interactions.
  • Express: Minimal Node.js web framework. Chosen for simplicity in setting up servers and handling HTTP webhook requests.
  • Vonage Messages API: Unified API for multi-channel messaging (SMS, MMS, WhatsApp). Chosen for robust features, global reach, and developer-friendly SDK.
  • Vonage Node.js SDK (@vonage/server-sdk@3.24.1): Simplifies Vonage API interaction. Current version actively maintained with regular updates.
  • ngrok or Cloudflare Tunnel: Tools to expose local servers to the internet. Essential for testing webhooks during development.
  • dotenv: Loads environment variables from .env files, keeping credentials secure.

Vonage SMS Pricing (US):

  • Outbound SMS: $0.0075 per message
  • Inbound SMS: $0.0069 per message
  • Total conversation cost: ~$0.0144 per send/receive pair

Source: Vonage Communications APIs Pricing, January 2025

System Architecture:

mermaid
graph LR
    subgraph Your Application
        A[Node.js Send Script] -- Sends SMS --> B(Vonage Node SDK);
        C[Express Server] -- Listens on Port 3000 --> D{Webhook Endpoint (/webhooks/inbound)};
        D -- Receives Inbound SMS --> E[Log Incoming Message];
        F[Express Server] -- Listens on Port 3000 --> G{Webhook Endpoint (/webhooks/status)};
        G -- Receives Status Updates --> H[Log Message Status];
    end

    subgraph Internet / Vonage Platform
        B -- API Call --> I(Vonage Messages API);
        I -- Delivers SMS --> J(User's Phone);
        J -- Sends Reply SMS --> K(Vonage Virtual Number);
        K -- Forwards via Webhook --> L(ngrok Tunnel);
        I -- Sends Status via Webhook --> L;
    end

    subgraph Developer Machine
        L -- Forwards Traffic --> C;
    end

    style Your Application fill:#f9f,stroke:#333,stroke-width:2px
    style J fill:#ccf,stroke:#333,stroke-width:2px
  • Sending: Your Node.js script uses the Vonage SDK to call the Messages API, which delivers the SMS.
  • Receiving: A user replies, Vonage receives it on your virtual number, sends the message data via an HTTP POST request (webhook) to your ngrok URL, ngrok forwards it to your local Express server, and your server handles the data. Status updates follow a similar path.

Prerequisites:

  • Node.js and npm: LTS version recommended. Download Node.js
  • Vonage API Account: Sign up at Vonage. Free trial available.
  • Vonage API Key and Secret: Find on the Vonage API Dashboard
  • Vonage Virtual Phone Number: Purchase from Vonage Dashboard (Numbers > Buy numbers). Ensure SMS capability in your target region.
  • ngrok or Cloudflare Tunnel: Choose one:
  • Vonage CLI (Optional but Recommended): Install via npm install -g @vonage/cli. Useful for managing applications and numbers.

Rate Limits: Vonage APIs default to 30 requests/second (2,592,000 SMS/day). Messages API sandbox limited to 1 message/second, 100 messages/month. Exceeding limits returns status code 1 (Throttled).

Source: Vonage API Support, Rate Limits Documentation

1. Set Up Your Vonage SMS Node.js Project

Create your project directory, initialize Node.js, and install dependencies.

  1. Create Project Directory: Open your terminal and create a new directory:

    bash
    mkdir vonage-sms-app
    cd vonage-sms-app
  2. Initialize Node.js Project: Create a package.json file with default settings:

    bash
    npm init -y
  3. Install Dependencies: Install required packages:

    bash
    npm install @vonage/server-sdk express dotenv
    • @vonage/server-sdk: Official Vonage library for Node.js (v3.24.1 current)
    • express: Web server framework for webhooks
    • dotenv: Loads environment variables from .env into process.env
  4. Create Project Files:

    bash
    # For Linux/macOS
    touch send-sms.js server.js .env .gitignore
    
    # For Windows (Command Prompt)
    type nul > send-sms.js
    type nul > server.js
    type nul > .env
    type nul > .gitignore
    
    # For Windows (PowerShell)
    New-Item send-sms.js -ItemType File
    New-Item server.js -ItemType File
    New-Item .env -ItemType File
    New-Item .gitignore -ItemType File
  5. Configure .gitignore: It's crucial to prevent sensitive information and unnecessary files from being committed to version control (like Git). Add the following lines to your .gitignore file:

    text
    # Environment variables
    .env
    
    # Node dependencies
    node_modules/
    
    # Log files
    *.log
    
    # OS generated files
    .DS_Store
    Thumbs.db
    
    # Vonage private key
    private.key
    *.key
  6. Set Up Environment Variables (.env): Open the .env file and add placeholders for your Vonage credentials and configuration. We will obtain these values in the next steps. Never commit this file to version control.

    dotenv
    # Vonage API Credentials (from Vonage Dashboard)
    VONAGE_API_KEY=YOUR_API_KEY
    VONAGE_API_SECRET=YOUR_API_SECRET
    
    # Vonage Application Credentials (created in the next section)
    VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
    VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root
    
    # Vonage Number and Recipient
    VONAGE_FROM_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
    TO_NUMBER=RECIPIENT_PHONE_NUMBER # e.g., 14155550100
    
    # Server Port
    PORT=3000
    • VONAGE_API_KEY, VONAGE_API_SECRET: Found directly on your Vonage dashboard homepage.
    • VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH: Will be generated when creating a Vonage Application.
    • VONAGE_FROM_NUMBER: The Vonage virtual number you purchased (use E.164 format, e.g., 12015550101).
    • TO_NUMBER: The destination phone number for sending test messages (use E.164 format).
    • PORT: The local port your Express server will listen on.

    Important: Remember to replace these placeholder values (YOUR_API_KEY, YOUR_API_SECRET, etc.) with your actual credentials and numbers obtained from the Vonage dashboard before running the application.

Project Structure:

Your project directory should now look like this:

vonage-sms-app/ ├── node_modules/ ├── .env ├── .gitignore ├── package-lock.json ├── package.json ├── send-sms.js └── server.js # (private.key will be added later)

This structure separates the sending logic (send-sms.js) from the receiving logic (server.js) and keeps configuration secure in .env.

2. Configure Vonage Messages API for SMS Marketing

Configure Vonage to use the Messages API and create a Vonage Application for SMS interactions.

  1. Set Default SMS API to Messages API: Vonage offers two SMS APIs (legacy SMS API and newer Messages API) with different webhook formats. Set Messages API as default:

    • Log in to the Vonage API Dashboard
    • Navigate to AccountAPI settings
    • Scroll to SMS settings
    • Under Default SMS Setting, select Messages API
    • Click Save changes
  2. Create a Vonage Application:

    • In the Vonage Dashboard, navigate to ApplicationsCreate a new application
    • Enter an Application name (e.g., NodeJS SMS App Tutorial)
    • Click Generate public and private key. This automatically downloads a private.key file. Save this file in the root of your vonage-sms-app project directory. The public key is stored by Vonage.
    • Enable Messages capability by toggling it on
    • You'll see fields for Inbound URL and Status URL. We need a publicly accessible URL here. We'll use ngrok for this during development. For now, enter temporary placeholders like https://example.com/webhooks/inbound and https://example.com/webhooks/status. We will update these later once ngrok is running.
    • Scroll down and click Generate new application
    • You will be taken to the application's page. Copy the Application ID displayed near the top
  3. Update .env File: Now, update your .env file with the actual values:

    • Paste your VONAGE_API_KEY and VONAGE_API_SECRET from the dashboard homepage
    • Paste the VONAGE_APPLICATION_ID you just copied
    • Ensure VONAGE_PRIVATE_KEY_PATH is set to ./private.key (assuming you saved the downloaded key file in the project root)
    • Fill in VONAGE_FROM_NUMBER with your Vonage virtual number
    • Fill in TO_NUMBER with your personal phone number for testing
  4. Link Your Vonage Number to the Application: Incoming messages to your Vonage number need to be routed to the correct application's webhook. For this SMS tutorial, linking the number under ""Messaging"" is the critical step.

    • Navigate to NumbersYour numbers in the dashboard
    • Find your Vonage virtual number
    • Click the Edit icon (pencil) next to the number
    • In the Messaging section's dropdown, select Application and choose your newly created application (e.g., NodeJS SMS App Tutorial). This ensures incoming SMS messages trigger your application's inbound webhook
    • (Optional for this tutorial) In the Voice section, you could also link the application if you intended to handle voice calls, but it's not required for SMS-only functionality
    • Click Save

Configuration is complete. We have told Vonage to use the Messages API, created an application with security keys, and linked our number to route SMS messages to this application's webhooks.

3. Send SMS Messages with Vonage Node.js SDK

Write code to send SMS messages using the Vonage Node.js SDK.

  1. Edit send-sms.js:

    javascript
    // send-sms.js
    'use strict';
    
    // Load environment variables from .env file
    require('dotenv').config();
    
    // Import the Vonage Server SDK
    const { Vonage } = require('@vonage/server-sdk');
    const { MESSAGES_SANDBOX_URL } = require('@vonage/server-sdk'); // Use if sandbox needed
    const fs = require('fs'); // Require file system module to read the private key
    
    // --- Configuration ---
    const vonageApiKey = process.env.VONAGE_API_KEY;
    const vonageApiSecret = process.env.VONAGE_API_SECRET;
    const vonageAppId = process.env.VONAGE_APPLICATION_ID;
    const privateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH;
    const fromNumber = process.env.VONAGE_FROM_NUMBER;
    const toNumber = process.env.TO_NUMBER;
    
    // Basic validation
    if (!vonageApiKey || !vonageApiSecret || !vonageAppId || !privateKeyPath || !fromNumber || !toNumber) {
        console.error('Error: Missing required environment variables. Check your .env file.');
        process.exit(1); // Exit if configuration is incomplete
    }
    
    // Read the private key file
    let privateKey;
    try {
        privateKey = fs.readFileSync(privateKeyPath);
    } catch (err) {
        console.error(`Error reading private key file at ${privateKeyPath}:`, err);
        process.exit(1);
    }
    
    // --- Initialize Vonage Client ---
    // Initialize Vonage Client using Application ID and Private Key for Messages API features (including status webhooks).
    const vonage = new Vonage({
        apiKey: vonageApiKey, // Still useful for some account-level operations, but Auth primarily via AppID/Key for Messages
        apiSecret: vonageApiSecret, // As above
        applicationId: vonageAppId,
        privateKey: privateKey,
        // Uncomment below for sandbox testing (requires specific setup)
        // apiHost: MESSAGES_SANDBOX_URL
    });
    
    // --- Define Message Content ---
    const messageText = `Hello from Vonage and Node.js! (${new Date().toLocaleTimeString()})`;
    
    // --- Send SMS Function ---
    async function sendSms() {
        console.log(`Attempting to send SMS from ${fromNumber} to ${toNumber}...`);
    
        try {
            const resp = await vonage.messages.send({
                channel: 'sms',
                message_type: 'text', // Type of message (text, image, etc.)
                to: toNumber,         // Recipient phone number
                from: fromNumber,     // Your Vonage virtual number
                text: messageText,    // The content of the SMS
                // client_ref: 'my-internal-tracking-id-123' // Optional: For tracking purposes
            });
    
            console.log('SMS submitted successfully!');
            console.log('Message UUID:', resp.message_uuid);
    
        } catch (err) {
            console.error('Error sending SMS:');
            // Log detailed error information if available
            if (err.response) {
                console.error('Status:', err.response.status);
                console.error('Status Text:', err.response.statusText);
                console.error('Data:', err.response.data);
                // Consult the Vonage API error documentation for details on specific error codes within err.response.data.
                // Link: https://developer.vonage.com/api-errors/messages
            } else {
                console.error(err);
            }
        }
    }
    
    // --- Execute Sending ---
    sendSms();

    Code Explanation:

    • require('dotenv').config();: Loads variables from .env into process.env
    • require('@vonage/server-sdk'): Imports the Vonage SDK
    • fs.readFileSync(privateKeyPath): Reads the content of your private key file. It's crucial that the content of the key is passed
    • new Vonage({...}): Initializes the Vonage client. For the Messages API involving applications and webhooks (like status updates), providing applicationId and privateKey is required for authentication
    • vonage.messages.send({...}): This is the core function for sending messages via the Messages API
      • channel: 'sms': Specifies the communication channel
      • message_type: 'text': Indicates a standard text message
      • to, from, text: Define the recipient, sender (your Vonage number), and message content
    • async/await with try...catch: Handles the asynchronous nature of the API call and provides basic error logging. We log the message_uuid on success, which is useful for tracking. The catch block attempts to log detailed error information from the API response
  2. Run the Send Script: Ensure your .env file contains valid credentials, then run:

    bash
    node send-sms.js

    You should see output confirming SMS submission and a Message UUID. The recipient receives the SMS within seconds.

4. Receive SMS Messages with Webhooks in Express

Set up the Express server to receive incoming SMS messages from Vonage.

  1. Edit server.js:

    javascript
    // server.js
    'use strict';
    
    // Load environment variables
    require('dotenv').config();
    
    // Import Express
    const express = require('express');
    const { json, urlencoded } = express; // Import body parsing middleware
    
    // --- Configuration ---
    const port = process.env.PORT || 3000; // Use port from .env or default to 3000
    
    // --- Initialize Express App ---
    const app = express();
    
    // --- Middleware ---
    // Enable parsing of JSON request bodies
    app.use(json());
    // Enable parsing of URL-encoded request bodies (standard for webhooks)
    app.use(urlencoded({ extended: true }));
    
    // --- Webhook Endpoints ---
    
    // Inbound Message Webhook (POST /webhooks/inbound)
    app.post('/webhooks/inbound', (req, res) => {
        console.log('--- Inbound Message Received ---');
        console.log('Timestamp:', new Date().toISOString());
        console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Pretty print JSON
    
        // Extract key information (adjust based on actual payload structure from Messages API)
        const from = req.body.from?.number || req.body.from; // Handle potential variations
        const to = req.body.to?.number || req.body.to;
        const text = req.body.message?.content?.text || req.body.text;
        const messageUuid = req.body.message_uuid || req.body.message?.message_uuid; // Check both possible locations
        const timestamp = req.body.timestamp;
    
        console.log(`\nMessage Details:`);
        console.log(`  From: ${from}`);
        console.log(`  To: ${to}`);
        console.log(`  Text: ""${text}""`); // Keep quotes for clarity
        console.log(`  Message UUID: ${messageUuid}`);
        console.log(`  Received At: ${timestamp}`);
    
        // --- IMPORTANT ---
        // Vonage expects a 200 OK response to acknowledge receipt of the webhook.
        // Failure to send a 200 OK will cause Vonage to retry sending the webhook.
        res.status(200).send('OK');
        // Alternatively, use res.status(200).end(); if no body is needed.
    });
    
    // Message Status Webhook (POST /webhooks/status)
    app.post('/webhooks/status', (req, res) => {
        console.log('--- Message Status Received ---');
        console.log('Timestamp:', new Date().toISOString());
        console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Pretty print JSON
    
        // Extract key status information from Messages API status webhook
        const messageUuid = req.body.message_uuid;
        const status = req.body.status;
        const timestamp = req.body.timestamp;
        const to = req.body.to?.number || req.body.to;
        const from = req.body.from?.number || req.body.from;
        const errorCode = req.body.error?.code;
        const errorReason = req.body.error?.reason;
    
        console.log(`\nStatus Details:`);
        console.log(`  Message UUID: ${messageUuid}`);
        console.log(`  To: ${to}`);
        console.log(`  From: ${from}`);
        console.log(`  Status: ${status}`);
        console.log(`  Timestamp: ${timestamp}`);
        if (errorCode) {
            console.log(`  Error Code: ${errorCode}`);
            console.log(`  Error Reason: ${errorReason}`);
        }
    
        // Acknowledge receipt
        res.status(200).send('OK');
    });
    
    // --- Root Endpoint (Optional: for testing server is running) ---
    app.get('/', (req, res) => {
        res.send(`Vonage SMS Webhook Server is running on port ${port}. Listening for POST requests on /webhooks/inbound and /webhooks/status.`);
    });
    
    // --- Start Server ---
    app.listen(port, () => {
        console.log(`Server listening at http://localhost:${port}`);
        console.log(`Webhook endpoints ready:`);
        console.log(`  Inbound SMS: POST http://localhost:${port}/webhooks/inbound`);
        console.log(`  Status Updates: POST http://localhost:${port}/webhooks/status`);
    });

    Code Explanation:

    • express(): Creates an Express application instance
    • app.use(json()) and app.use(urlencoded({ extended: true })): Middleware essential for parsing incoming request bodies. Webhooks often send data as JSON or URL-encoded form data
    • app.post('/webhooks/inbound', ...): Defines a route handler for POST requests to /webhooks/inbound. This is where Vonage will send data for incoming SMS messages
      • We log the entire request body (req.body) for inspection
      • We extract key fields like sender (from), recipient (to), message content (text), and unique ID (message_uuid). The exact structure might vary slightly based on the Messages API payload, so logging the whole body is helpful for debugging
      • res.status(200).send('OK'): Crucially, we send a 200 OK HTTP status code back to Vonage. This acknowledges successful receipt. If Vonage doesn't receive a 200, it assumes delivery failed and will retry, potentially causing duplicate processing on your end
    • app.post('/webhooks/status', ...): Defines a route handler for POST requests to /webhooks/status. This is where Vonage sends updates about the delivery status of outgoing messages you sent (e.g., submitted, delivered, failed, rejected)
      • We log the body and extract relevant status information
      • Again, sending 200 OK is mandatory
    • app.listen(port, ...): Starts the Express server, making it listen for connections on the specified port

5. Exposing the Local Server with ngrok or Cloudflare Tunnel

Your local server (http://localhost:3000) isn't publicly accessible. Choose a tunneling solution:

Option A: ngrok (Quick Setup)

  1. Start Your Express Server:

    bash
    node server.js
  2. Start ngrok: In a second terminal:

    bash
    ngrok http 3000
  3. Copy the ngrok Forwarding URL: ngrok will display output similar to this:

    Session Status online Account Your Name (Plan: Free) Version x.x.x Region United States (us-cal-1) Web Interface http://127.0.0.1:4040 Forwarding https://<random-string>.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://<random-string>.ngrok-free.app URL. This is your public URL. Note: Remember, ngrok provides a temporary URL suitable only for development and testing. For a production application, you will need a stable, publicly accessible URL provided by your hosting environment. Free ngrok accounts also generate a new random URL each time you restart ngrok.

    ngrok Free Tier Limits (2025):

    • 1 GB/month bandwidth
    • URL changes on restart
    • Limited to 1 domain

    Source: ngrok pricing documentation

Option B: Cloudflare Tunnel (Production-Ready Alternative)

  1. Install cloudflared:

    bash
    # macOS (Homebrew)
    brew install cloudflare/cloudflare/cloudflared
    
    # Linux
    wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
    sudo dpkg -i cloudflared-linux-amd64.deb
    
    # Windows (Chocolatey)
    choco install cloudflared
  2. Authenticate:

    bash
    cloudflared tunnel login
  3. Create and Run Tunnel:

    bash
    cloudflared tunnel --url http://localhost:3000
  4. Copy the Cloudflare URL: Copy the https://<random-string>.trycloudflare.com URL from the output.

    Cloudflare Tunnel Benefits:

    • Free for up to 50 users
    • Unlimited bandwidth
    • Built-in DDoS protection
    • More stable than ngrok free tier

    Source: Cloudflare Zero Trust documentation

Update Vonage Application Webhook URLs

For either option:

  1. Go to your Vonage Application settings (Applications → Your Application)
  2. Click Edit
  3. Update webhook URLs with your tunnel URL:
    • Inbound URL: https://<your-tunnel-url>/webhooks/inbound
    • Status URL: https://<your-tunnel-url>/webhooks/status
  4. Click Save changes

Your local server now receives webhooks from Vonage via your chosen tunnel.

6. Verification and Testing

Test both sending and receiving functionality.

  1. Test Sending (Again):

    • Run node send-sms.js in a terminal
    • Check your phone for the SMS
    • Check the terminal where server.js is running. You should see logs under --- Message Status Received --- showing the status updates for the message you just sent (e.g., submitted, delivered)
  2. Test Receiving:

    • Using the phone that received the SMS (or any phone), send an SMS message to your Vonage virtual number (VONAGE_FROM_NUMBER in your .env)
    • Watch the terminal where server.js is running
    • You should see logs under --- Inbound Message Received --- containing the details of the message you just sent from your phone
  3. Inspect with ngrok Web Interface:

    • Open http://127.0.0.1:4040 (the Web Interface URL shown when starting ngrok) in your browser
    • You can see detailed logs of the HTTP requests being forwarded by ngrok, including headers and bodies, which is very useful for debugging webhook issues

7. Implement TCPA Compliance for SMS Marketing (Required for US Campaigns)

If you're sending marketing messages or promotional SMS to US recipients, comply with the Telephone Consumer Protection Act (TCPA) and FCC regulations to avoid costly violations.

Critical FCC Opt-Out Rule (Effective April 11, 2025)

New Requirements:

  1. 10-Day Processing Window: Honor opt-out requests within 10 business days (reduced from 30 days)

  2. Expanded Opt-Out Language: Recognize diverse opt-out phrases beyond "STOP":

    • "Leave me alone"
    • "Don't text me anymore"
    • "Please stop"
    • "Unsubscribe"
    • "Remove me"
    • Any message with clear opt-out intent
  3. Confirmation Message Rules:

    • Send one confirmation message within 5 minutes of opt-out
    • Exclude promotional content from confirmation
    • If no response, assume full opt-out
  4. Record Retention: Maintain opt-out documentation for 4 years (TCPA statute of limitations)

Penalties for Non-Compliance:

  • $500-$1,500 per violation (per message)
  • No requirement to prove actual damages
  • Class-action lawsuit risk

Implementation Example:

Add this to your /webhooks/inbound handler in server.js:

javascript
// Enhanced inbound handler with TCPA opt-out detection
app.post('/webhooks/inbound', async (req, res) => {
    console.log('--- Inbound Message Received ---');
    const from = req.body.from?.number || req.body.from;
    const text = (req.body.message?.content?.text || req.body.text || '').toLowerCase().trim();

    // TCPA-compliant opt-out detection
    const optOutKeywords = [
        'stop', 'stopall', 'unsubscribe', 'cancel', 'end', 'quit',
        'leave me alone', "don't text me", 'opt out', 'opt-out', 'remove me'
    ];

    const isOptOut = optOutKeywords.some(keyword => text.includes(keyword));

    if (isOptOut) {
        console.log(`⚠️  OPT-OUT REQUEST from ${from}`);

        // TODO: Store opt-out in database with timestamp
        // TODO: Remove from marketing lists
        // TODO: Send confirmation message (within 5 minutes)

        // Example confirmation (implement with your sendSms function):
        // await sendConfirmation(from, "You've been unsubscribed. Reply START to resubscribe.");
    }

    res.status(200).send('OK');
});

Best Practices:

  • Database Integration: Store opt-outs with timestamps for audit trail
  • Cross-Channel Application: Apply opt-outs across SMS, email, and phone
  • Staff Training: Ensure team recognizes opt-out intent in manual reviews
  • Regular Audits: Review opt-out processes quarterly

Sources:

  • FCC Opt-Out Rule (Effective April 11, 2025)
  • TCPA Compliance Guidelines, ActiveProspect
  • Bryan Cave Leighton Paisner legal analysis

8. Handle Vonage API Rate Limits for Bulk SMS Campaigns

Respect Vonage's API rate limits to avoid throttling errors.

Vonage Rate Limits:

  • Default: 30 requests/second (2,592,000 messages/day)
  • Sandbox: 1 request/second, 100 messages/month
  • Exceeded limit response: HTTP 429 or Status code 1 (Throttled)

Source: Vonage API Support Documentation

Implementation with p-limit:

Install the rate limiting library:

bash
npm install p-limit

Update your sending logic for bulk campaigns:

javascript
// bulk-send-sms.js
'use strict';

require('dotenv').config();
const { Vonage } = require('@vonage/server-sdk');
const pLimit = require('p-limit');
const fs = require('fs');

// Initialize Vonage client (same as send-sms.js)
const privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH);
const vonage = new Vonage({
    apiKey: process.env.VONAGE_API_KEY,
    apiSecret: process.env.VONAGE_API_SECRET,
    applicationId: process.env.VONAGE_APPLICATION_ID,
    privateKey: privateKey,
});

// Rate limiting: 30 concurrent requests max (matches Vonage's 30 req/sec limit)
const limit = pLimit(30);

async function sendBulkSMS(recipients, messageText) {
    const tasks = recipients.map(recipient =>
        limit(() =>
            vonage.messages.send({
                channel: 'sms',
                message_type: 'text',
                to: recipient,
                from: process.env.VONAGE_FROM_NUMBER,
                text: messageText,
            })
            .then(resp => ({
                success: true,
                recipient,
                message_uuid: resp.message_uuid
            }))
            .catch(err => ({
                success: false,
                recipient,
                error: err.message
            }))
        )
    );

    const results = await Promise.all(tasks);

    const successful = results.filter(r => r.success).length;
    const failed = results.filter(r => !r.success).length;

    console.log(`\nBulk Send Complete:`);
    console.log(`  ✓ Successful: ${successful}`);
    console.log(`  ✗ Failed: ${failed}`);

    return results;
}

// Example usage
const recipients = ['+14155551234', '+14155555678', '+14155559012'];
const message = 'Hello from our marketing campaign! Reply STOP to unsubscribe.';

sendBulkSMS(recipients, message)
    .then(results => console.log('All sends completed'))
    .catch(err => console.error('Bulk send error:', err));

This implementation prevents exceeding Vonage's rate limits while maximizing throughput.

9. Error Handling and Logging (Enhanced)

The guide uses basic console.log for demonstration. For production:

  • Robust Logging: Use libraries like Winston or Pino for structured logging (e.g., JSON format), different log levels (debug, info, warn, error), and configurable outputs (console, file, external logging services)
  • SDK Error Handling: The try...catch block in send-sms.js catches errors during the API call. Inspecting err.response.data often provides specific Vonage error codes and messages. Refer to the Vonage API documentation for error code details (link added in the code comments)
  • Webhook Errors: Ensure your webhook endpoints always return a 200 OK. If your internal processing fails (e.g., database error), log the error thoroughly but still return 200 to Vonage to prevent retries. Implement a separate mechanism (like a queue or background job) to handle failed webhook processing later
  • Input Validation: Sanitize and validate data received in webhooks (req.body) before processing it to prevent errors or security issues. Libraries like joi or express-validator can help

10. Security Considerations

  • Credentials: Never hardcode API keys, secrets, or private keys in your source code. Use environment variables (.env) and ensure .env and *.key are in your .gitignore. In production, use secure secret management systems provided by your hosting platform (e.g., AWS Secrets Manager, Azure Key Vault, HashiCorp Vault)
  • Private Key File Permissions: Ensure your private.key file has restrictive permissions (e.g., chmod 600 on Linux/macOS) so only the owner can read it. This prevents other users on the system from accessing your key
  • Webhook Security: While not implemented here for simplicity, production webhooks should be secured. Vonage supports Signed Webhooks using JWT. Your server would verify the signature of incoming requests using your Vonage signature secret to ensure they genuinely came from Vonage. See Vonage Signed Webhooks documentation
  • Rate Limiting: Protect your webhook endpoints from abuse by implementing rate limiting (e.g., using express-rate-limit)
  • Input Sanitization: Always sanitize text input from SMS messages before displaying it or storing it to prevent Cross-Site Scripting (XSS) or other injection attacks if this data is ever rendered in a web context

11. Troubleshooting and Caveats

  • Incorrect Credentials: Verify VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, and the content/path of VONAGE_PRIVATE_KEY_PATH in .env. Ensure dotenv is loading correctly (e.g., require('dotenv').config() is called early)
  • ngrok Issues:
    • Ensure ngrok is running and forwarding to the correct port (3000)
    • Ensure the ngrok URL in the Vonage Application settings is correct (HTTPS) and includes the /webhooks/inbound or /webhooks/status path
    • Free ngrok URLs change on restart – update Vonage settings if you restart ngrok
    • Firewalls might block ngrok traffic
    • Remember ngrok is for development/testing; use a stable public URL in production
  • Number Not Linked: Ensure your Vonage number is correctly linked to your Vonage Application under the Messaging settings in the dashboard
  • API Default Setting: Verify the Default SMS Setting in Vonage account settings is set to Messages API. Using the wrong API setting will result in webhook payloads your code doesn't expect
  • Missing 200 OK: If webhooks seem to arrive multiple times or Vonage reports failures, ensure your /webhooks/inbound and /webhooks/status handlers are reliably sending res.status(200).send('OK') or res.status(200).end(). Check the ngrok web interface (http://127.0.0.1:4040) for the responses your server sent
  • Alphanumeric Sender IDs: Sending from a name (e.g., MyApp) instead of a number is supported in some countries but not others (like the US/Canada). Check Vonage country-specific guidelines. Use your Vonage number for maximum compatibility
  • Message Encoding: Special characters or emoji might require type: 'unicode' in the sendSms options (though the Messages API generally handles this well). Test thoroughly
  • Rate Limits: Vonage applies rate limits to API calls and message sending. Check their documentation if you encounter throttling errors (often HTTP 429)

12. Deploy Your SMS Marketing Platform to Production

Deploy the server.js component to production:

  • Hosting: Heroku, AWS (EC2, Lambda + API Gateway), Google Cloud (App Engine, Cloud Run), Azure (App Service), or DigitalOcean (App Platform)
  • Environment Variables: Use platform-specific secret management (AWS Secrets Manager, Azure Key Vault). Never deploy .env files
  • Persistent URL: Replace ngrok/Cloudflare with production domain for Vonage webhook URLs
  • Process Management: Use PM2 for Node.js process reliability (auto-restart, clustering, logging)
  • CI/CD: Automate testing and deployment with GitHub Actions, GitLab CI, or Jenkins
  • Database: Store message history and opt-outs with PostgreSQL or MongoDB. Use Prisma or Sequelize ORM
  • Monitoring: Integrate Sentry, Datadog, or similar for error tracking and performance monitoring

13. Next Steps: Scale Your SMS Marketing Platform

You've built a Node.js SMS messaging application using Express and Vonage Messages API with TCPA compliance and rate limiting – a complete foundation for SMS marketing campaigns and two-way communication platforms.

Potential Enhancements:

  • Automated Chatbot Replies: Implement natural language processing to respond intelligently to incoming messages
  • Database Integration: Store message logs, user data, conversation history, and opt-out records with PostgreSQL or MongoDB
  • Admin Dashboard: Build a web UI to manage campaigns, view analytics, and export reports
  • Advanced Monitoring: Integrate Sentry for error tracking, Datadog for performance monitoring, and Grafana for custom dashboards
  • Webhook Security: Implement JWT signature verification for production webhook authentication
  • Complete SMS Marketing Platform: Build enterprise features:
    • Campaign scheduling with cron jobs or AWS EventBridge
    • Bulk messaging with real-time progress tracking and retry logic
    • Opt-out management dashboard with automated compliance auditing
    • Analytics and reporting: delivery rates, opt-out rates, engagement metrics, conversion tracking
    • A/B testing capabilities for message optimization
    • Drip campaign automation with conditional logic
    • Segmentation engine for targeted messaging
    • SMS shortlink tracking with UTM parameters

SMS Marketing Campaign Checklist:

Before launching production campaigns:

  • ☐ Implement TCPA-compliant opt-out detection and handling
  • ☐ Maintain consent records with timestamps for all recipients
  • ☐ Configure opt-out database with 4-year retention policy
  • ☐ Set up rate limiting (30 requests/second maximum)
  • ☐ Test expanded opt-out keywords ("STOP," "UNSUBSCRIBE," "Leave me alone," etc.)
  • ☐ Deploy to production with persistent webhook URLs (not ngrok)
  • ☐ Configure monitoring, logging, and error alerting
  • ☐ Calculate campaign costs: ~$0.0144 per conversation × expected volume
  • ☐ Train team on TCPA compliance and FCC regulations
  • ☐ Set up automated compliance reporting

Related Resources:

Expand your Vonage SMS implementation with these guides: