messaging channels

Sent logo
Sent TeamMar 8, 2026 / messaging channels / Article

Send SMS and WhatsApp Messages with Node.js, Express, and Vonage

Build a Node.js Express application to send SMS and WhatsApp messages using the Vonage Messages API. Includes webhook handling, signature verification, and production security best practices.

Build a Node.js Express application to send SMS and WhatsApp messages via the Vonage Messages API. This guide covers project setup, sending messages through different channels, handling incoming messages and status updates via webhooks, and implementing essential production considerations like security and error handling.

By the end of this tutorial, you'll have a functional application capable of programmatically sending SMS and WhatsApp messages and receiving delivery status updates and inbound messages from users. This provides a foundation for building chatbots, notification systems, two-factor authentication flows, and other communication-driven features.

Project Overview and Goals

What You'll Build:

  • A Node.js Express server
  • Integration with the Vonage Messages API using the official Node.js SDK
  • Functionality to send outbound SMS messages
  • Functionality to send outbound WhatsApp messages (using the Vonage Sandbox initially)
  • Webhook endpoints to receive inbound messages from users (SMS and WhatsApp)
  • Webhook endpoints to receive message status updates (e.g., delivered, failed)
  • Secure handling of API credentials and webhook signature verification

Problem Solved:

This application provides a unified way to interact with customers or users over two popular messaging channels – SMS and WhatsApp – directly from your Node.js backend. It abstracts the complexities of the Vonage API into reusable functions and provides a basic structure for handling asynchronous communication events.

Technologies Used:

  • Node.js: A JavaScript runtime environment for building server-side applications (Version 18 or higher recommended)
  • Express: A minimal and flexible Node.js web application framework used to build the server and API endpoints/webhooks
  • Vonage Messages API: A powerful API enabling communication across multiple channels (SMS, MMS, WhatsApp, Facebook Messenger, Viber) through a single interface
  • Vonage Node.js Server SDK (@vonage/server-sdk): Simplifies interaction with Vonage APIs, including authentication and sending messages
  • Vonage Messages SDK (@vonage/messages): Provides convenient classes for constructing messages for specific channels (like WhatsAppText)
  • Vonage JWT SDK (@vonage/jwt): Used for verifying the signature of incoming webhook requests to ensure they originate from Vonage
  • dotenv: A module to load environment variables from a .env file into process.env
  • ngrok: A tool to expose local servers to the internet, necessary for testing webhooks during development

System Architecture:

text
+-----------------+      HTTP Request       +---------------------+      Vonage API Call      +-----------------+
| Your Application| ---------------------> | Node.js/Express App | -------------------------> | Vonage Platform |
| (e.g., Frontend)|      (API Call)        |  (Vonage SDK)       |      (Messages API)     |                 |
+-----------------+                        +---------------------+                           +-------+---------+
                                                     |                                               |
                                                     | Webhook Call (POST)                           | Send Message
                                                     V                                               V
+-----------------+      Webhook Call (POST) +-------+---------+                           +-------+---------+
| Vonage Platform | <----------------------- | Node.js/Express App |                           | User's Device   |
|                 | (Inbound Msg / Status) |  (Webhook Handler)  |                           | (SMS/WhatsApp)  |
+-----------------+                        +---------------------+                           +-----------------+

Prerequisites:

  • Vonage API Account: Sign up for free at Vonage API Dashboard. You get free credit to start.
  • Node.js and npm: Install Node.js (v18 or higher recommended, includes npm) from nodejs.org.
  • ngrok: Install ngrok from ngrok.com and create a free account to authenticate your agent.
  • A Vonage Phone Number: Purchase an SMS-capable virtual number from the Vonage Dashboard (Numbers → Buy numbers).
  • WhatsApp Enabled Device: A smartphone with WhatsApp installed for testing.

How Do You Set Up a Node.js Project for Vonage Messaging?

Initialize your Node.js project and install the necessary dependencies.

  1. Create Project Directory: Open your terminal or command prompt and create a new directory for your project, then navigate into it:

    bash
    mkdir vonage-node-messaging
    cd vonage-node-messaging
  2. Initialize Node.js Project: Initialize the project using npm. The -y flag accepts default settings.

    bash
    npm init -y

    This creates a package.json file.

  3. Install Dependencies: Install Express, the Vonage SDKs, and dotenv:

    bash
    npm install express @vonage/server-sdk @vonage/messages @vonage/jwt dotenv
    • express: Web framework
    • @vonage/server-sdk: Core Vonage SDK for authentication and API calls
    • @vonage/messages: Specific classes for constructing message types (e.g., WhatsAppText)
    • @vonage/jwt: For webhook signature verification
    • dotenv: For managing environment variables
  4. Create Project Structure: Create a basic structure for your code:

    bash
    mkdir src
    touch src/server.js
    touch .env
    touch .gitignore
    • src/server.js: Your main application code
    • .env: Stores sensitive credentials and configuration
    • .gitignore: Prevents committing sensitive files (like .env) and node_modules to version control
  5. Configure .gitignore: Add the following lines to your .gitignore file to avoid committing sensitive information and dependencies:

    text
    # Dependencies
    node_modules/
    
    # Environment Variables
    .env
    
    # Vonage Private Key
    private.key
    
    # Operating System Files
    .DS_Store
    Thumbs.db
  6. Set up Vonage Application: Create a Vonage Application to group your numbers and configurations, and generate authentication credentials.

    • Navigate to Applications in your Vonage API Dashboard.
    • Click + Create a new application.
    • Give your application a name (e.g., "Node Express Messaging").
    • Click Generate public and private key. This will automatically download a private.key file. Save this file securely in the root of your project directory (e.g., alongside package.json). Crucially, ensure private.key is listed in your .gitignore file and is NEVER committed to version control. For production, load this key securely (e.g., from environment variables, secrets management, or a secure volume mount) rather than directly from the filesystem. The public key is stored by Vonage.
    • Enable the Messages capability by toggling it on.
    • You'll see fields for Inbound URL and Status URL. Fill these in the next step after starting ngrok. Leave them blank for now or use temporary placeholders like http://localhost.
    • Click Generate new application.
    • On the next screen, Link the Vonage virtual number you purchased earlier to this application. Find your number and click the "Link" button.
    • Note down the Application ID displayed on this page.
  7. Configure SMS API Settings: Ensure your account uses the Messages API for SMS by default, as recommended by Vonage documentation for new integrations.

    • In the Vonage Dashboard, navigate to API Settings.
    • Scroll down to the SMS Settings section.
    • Under Default SMS Setting, select Messages API.
    • Click Save changes.
  8. Set up WhatsApp Sandbox (for Testing): For testing WhatsApp without a full business setup, use the Vonage Sandbox.

    • Navigate to Messages API Sandbox in the Vonage Dashboard.
    • Follow the instructions to activate the Sandbox by sending a specific message from your WhatsApp number to the provided Sandbox number. This "allowlists" your number for testing.
    • Keep this page open, as you'll need the Sandbox number and will configure webhooks here too.
  9. Start ngrok: To receive webhooks from Vonage on your local machine, expose your local server to the internet.

    • Open a new terminal window (keep your project terminal open).

    • Run ngrok, specifying the port your Express server will listen on (use port 3000):

      bash
      # Make sure you've authenticated ngrok if it's your first time
      # ngrok config add-authtoken YOUR_AUTH_TOKEN
      ngrok http 3000
    • ngrok will display output including a Forwarding URL that looks like https://<random-string>.ngrok-free.app (or similar, depending on your plan/version). Copy this HTTPS URL.

  10. Configure Webhook URLs in Vonage:

    • Vonage Application: Go back to your application settings in the Vonage Dashboard (Applications → Your Application Name).
      • Enter your ngrok HTTPS URL followed by /webhooks/inbound into the Inbound URL field (e.g., https://<random-string>.ngrok-free.app/webhooks/inbound).
      • Enter your ngrok HTTPS URL followed by /webhooks/status into the Status URL field (e.g., https://<random-string>.ngrok-free.app/webhooks/status).
      • Click Save changes.
    • WhatsApp Sandbox: Go back to the Messages API Sandbox page in the Vonage Dashboard.
      • Enter the same URLs (.../webhooks/inbound and .../webhooks/status) into the respective webhook fields on the Sandbox configuration page.
      • Click Save webhooks.
  11. Configure Environment Variables (.env): Open the .env file you created and add the following variables, replacing the placeholder values with your actual credentials:

    dotenv
    # Vonage API Credentials
    VONAGE_API_KEY=YOUR_API_KEY
    VONAGE_API_SECRET=YOUR_API_SECRET
    
    # Vonage Application Credentials
    VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
    VONAGE_PRIVATE_KEY=./private.key # Path relative to project root – load securely in production!
    
    # Vonage Numbers
    VONAGE_SMS_FROM_NUMBER=YOUR_VONAGE_SMS_NUMBER # Your purchased Vonage number in E.164 format (e.g., 14155550100)
    VONAGE_WHATSAPP_SANDBOX_NUMBER=14157386102 # The Vonage WhatsApp Sandbox number
    
    # Webhook Security
    VONAGE_API_SIGNATURE_SECRET=YOUR_SIGNATURE_SECRET
    
    # Server Configuration
    PORT=3000
    • VONAGE_API_KEY & VONAGE_API_SECRET: Found at the top of your Vonage API Dashboard.
    • VONAGE_APPLICATION_ID: The ID generated when you created the Vonage Application.
    • VONAGE_PRIVATE_KEY: The path to the private.key file you downloaded and saved in your project root. Ensure this path is correct relative to where you run the node process.
    • VONAGE_SMS_FROM_NUMBER: Your purchased Vonage virtual number (without + or leading 00).
    • VONAGE_WHATSAPP_SANDBOX_NUMBER: The specific number provided by the Vonage WhatsApp Sandbox. For production WhatsApp, this would be your registered WhatsApp Business number.
    • VONAGE_API_SIGNATURE_SECRET: Found in the Vonage API Dashboard under API SettingsWebhook signature secret. Click "Edit" or "Generate" if needed.
    • PORT: The port your Express server will run on (must match the ngrok port).

How Do You Implement SMS and WhatsApp Messaging in Express?

Now, let's write the code in src/server.js to initialize the server, configure Vonage, send messages, and handle webhooks.

javascript
// src/server.js
require('dotenv').config(); // Load environment variables from .env file first

const express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const { WhatsAppText } = require('@vonage/messages');
const { verifySignature } = require('@vonage/jwt');

// --- Initialization ---
const app = express();
// Note: We apply express.json() globally AFTER raw body middleware for webhook routes
// app.use(express.json()); // Moved lower
app.use(express.urlencoded({ extended: true })); // Middleware to parse URL-encoded bodies

// Initialize Vonage Client
// Ensure all required environment variables are loaded correctly
if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY) {
    console.error('ERROR: Missing Vonage API credentials in .env file.');
    process.exit(1); // Exit if essential credentials are missing
}
if (!process.env.VONAGE_API_SIGNATURE_SECRET) {
    // Signature secret is mandatory for webhook security.
    console.error('ERROR: Missing VONAGE_API_SIGNATURE_SECRET in .env file. Webhooks cannot be verified.');
    process.exit(1);
}


const 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 to your private key file
},
{
    // Optional: Set custom logger or other options if needed
    // logger: customLogger,
    // Optional: Use the sandbox host for ALL messages if testing ONLY WhatsApp Sandbox features extensively
    // apiHost: 'https://messages-sandbox.nexmo.com' // NOTE: Remove/comment this for production SMS or non-sandbox WhatsApp
});

// --- Helper Functions ---

/**
 * Sends an SMS message using the Vonage Messages API.
 * @param {string} toNumber - The recipient's phone number in E.164 format.
 * @param {string} messageText - The text content of the message.
 */
async function sendSms(toNumber, messageText) {
    console.log(`Attempting to send SMS to ${toNumber}...`);
    if (!process.env.VONAGE_SMS_FROM_NUMBER) {
        console.error('ERROR: VONAGE_SMS_FROM_NUMBER is not set in .env');
        return;
    }
    try {
        const resp = await vonage.messages.send({
            message_type: "text",
            text: messageText,
            to: toNumber,
            from: process.env.VONAGE_SMS_FROM_NUMBER,
            channel: "sms"
        });
        console.log(`SMS sent successfully to ${toNumber}. Message UUID: ${resp.messageUuid}`);
    } catch (err) {
        console.error(`Error sending SMS to ${toNumber}:`, err?.response?.data || err.message);
        // Consider adding more robust error handling/retry logic here
    }
}

/**
 * Sends a WhatsApp message using the Vonage Messages API.
 * Uses the Sandbox number by default.
 * @param {string} toNumber - The recipient's WhatsApp-enabled number in E.164 format (must be allowlisted in Sandbox).
 * @param {string} messageText - The text content of the message.
 */
async function sendWhatsApp(toNumber, messageText) {
    console.log(`Attempting to send WhatsApp message to ${toNumber} via Sandbox...`);
    if (!process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER) {
        console.error('ERROR: VONAGE_WHATSAPP_SANDBOX_NUMBER is not set in .env');
        return;
    }
    try {
        // NOTE: For production, 'from' would be your registered WhatsApp Business number.
        // The host might also need adjustment if not using the global endpoint.
        // The 'apiHost' in the Vonage client initialization can also control this.
        const resp = await vonage.messages.send(
            new WhatsAppText({
                text: messageText,
                to: toNumber,
                from: process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER,
                // Optional: client_ref for your own tracking
                // client_ref: `my-whatsapp-message-${Date.now()}`
            })
            // Optional second argument for sandbox override if client wasn't initialized with sandbox host:
            // , { apiHost: 'https://messages-sandbox.nexmo.com' }
        );
        console.log(`WhatsApp message sent successfully to ${toNumber}. Message UUID: ${resp.messageUuid}`);
    } catch (err) {
        console.error(`Error sending WhatsApp message to ${toNumber}:`, err?.response?.data || err.message);
        // Consider adding more robust error handling/retry logic here
    }
}

/**
 * Verifies the signature of an incoming Vonage webhook request.
 * @param {express.Request} req - The Express request object.
 * @returns {boolean} - True if the signature is valid, false otherwise.
 */
function verifyWebhookSignature(req) {
    try {
        // IMPORTANT: Use raw body for signature verification
        // We need to configure Express to make the raw body available.
        // See rawBodyMiddleware configuration below.
        if (!req.rawBody) {
            console.error('ERROR: Raw body not available for signature verification. Ensure rawBodyMiddleware is used.');
            return false;
        }
        if (!process.env.VONAGE_API_SIGNATURE_SECRET) {
            // This should ideally be caught at startup, but double-check here.
            console.error('ERROR: VONAGE_API_SIGNATURE_SECRET is not set. Cannot verify webhook signature.');
            // Fail verification if the secret is missing. Verification is mandatory for security.
            return false;
        }

        // Clone headers and convert to lowercase keys as expected by verifySignature
        const headers = {};
        for (const key in req.headers) {
            headers[key.toLowerCase()] = req.headers[key];
        }

        // Extract token from Authorization header (format: ""Bearer <JWT>"")
        const token = headers['authorization']?.split(' ')[1];
        if (!token) {
            console.error('Webhook Error: Authorization header missing or malformed.');
            return false;
        }

        // Verify the signature using the raw body
        const isValid = verifySignature(token, process.env.VONAGE_API_SIGNATURE_SECRET, req.rawBody.toString());
        if (!isValid) {
             console.error('Webhook Error: Invalid Signature');
        }
        return isValid;

    } catch (error) {
        console.error('Error verifying webhook signature:', error.message);
        return false;
    }
}


// --- Middleware for Raw Body ---
// This needs to run BEFORE express.json() for routes requiring signature verification
const rawBodyMiddleware = express.raw({
    type: 'application/json',
    verify: (req, res, buf) => {
        // Store the raw buffer on the request object
        req.rawBody = buf;
    }
});


// --- Webhook Endpoints ---

// Apply raw body parsing ONLY to webhook routes, THEN apply JSON parsing
app.post('/webhooks/inbound', rawBodyMiddleware, express.json(), async (req, res) => {
    console.log('Received Inbound Webhook:');
    console.log(JSON.stringify(req.body, null, 2)); // Log the full body

    // 1. Verify Signature (CRITICAL for security)
    if (!verifyWebhookSignature(req)) {
        console.log('Webhook signature verification failed. Responding 401.');
        return res.status(401).send('Invalid Signature');
    }
    console.log('Webhook signature verified successfully.');

    // 2. Process the message based on channel
    const { from, channel, message } = req.body;

    if (channel === 'sms' || channel === 'whatsapp') {
        const sender = from.number || from.id; // WhatsApp uses 'id', SMS uses 'number'
        const messageContent = message?.content?.text || '[Non-text message content]';

        console.log(`Received ${channel.toUpperCase()} message from ${sender}: ""${messageContent}""`);

        // 3. (Optional) Send an automated reply
        const replyText = `Thanks for your ${channel.toUpperCase()} message! We received: ""${messageContent}""`;
        if (channel === 'sms' && process.env.VONAGE_SMS_FROM_NUMBER) {
             // IMPORTANT: Check if 'from.number' exists before replying to SMS
             if (from.number) {
                 await sendSms(from.number, replyText);
             } else {
                 console.warn('Cannot reply to SMS: Sender number missing in webhook payload.');
             }
        } else if (channel === 'whatsapp' && process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER) {
             // IMPORTANT: Check if 'from.id' exists before replying to WhatsApp
             if (from.id) {
                 // Check if within 24-hour window for non-template messages (Sandbox usually allows replies)
                 await sendWhatsApp(from.id, replyText);
             } else {
                  console.warn('Cannot reply to WhatsApp: Sender ID missing in webhook payload.');
             }
        }
    } else {
        console.log(`Received webhook for unhandled channel: ${channel}`);
    }

    // 4. Acknowledge receipt with a 200 OK
    // Vonage webhooks expect a 200 OK response quickly, otherwise they retry.
    res.status(200).end();
});

app.post('/webhooks/status', rawBodyMiddleware, express.json(), (req, res) => {
    console.log('Received Status Webhook:');
    console.log(JSON.stringify(req.body, null, 2)); // Log the full body

    // 1. Verify Signature (CRITICAL for security)
    if (!verifyWebhookSignature(req)) {
        console.log('Webhook signature verification failed. Responding 401.');
        return res.status(401).send('Invalid Signature');
    }
     console.log('Webhook signature verified successfully.');

    // 2. Process the status update (e.g., update database, trigger alerts)
    const { message_uuid, status, timestamp, error } = req.body;
    console.log(`Status update for Message UUID ${message_uuid}: ${status} at ${timestamp}`);
    if (error) {
        console.error(`Error details: Code=${error.code}, Reason=${error.reason}`);
    }

    // 3. Acknowledge receipt with a 200 OK
    res.status(200).end();
});


// --- API Endpoints (Example) ---

// Apply standard JSON parsing middleware to API routes (and any other non-webhook routes)
app.use(express.json());

// Note: The following comments use apidoc syntax (https://apidocjs.com/).
// You can use the 'apidoc' tool to generate API documentation from these comments.
/**
 * @api {post} /api/send/sms Send an SMS message
 * @apiName SendSms
 * @apiGroup Messages
 *
 * @apiBody {String} to Recipient phone number in E.164 format.
 * @apiBody {String} message Text content of the SMS.
 *
 * @apiSuccess {String} status Success message.
 * @apiError {String} error Error description.
 */
app.post('/api/send/sms', async (req, res) => {
    const { to, message } = req.body;

    // Basic Input Validation
    if (!to || !message) {
        return res.status(400).json({ error: 'Missing ""to"" or ""message"" in request body.' });
    }
    // Basic format check (11-15 digits). For robust E.164 validation, use a library like libphonenumber-js.
    if (!/^\d{11,15}$/.test(to)) {
         return res.status(400).json({ error: 'Invalid ""to"" number format. The Vonage SDK expects E.164 format without the leading \'+\'.' });
    }


    // TODO: Add Authentication/Authorization middleware here for production
    console.log(`API request to send SMS to ${to}`);

    try {
        await sendSms(to, message);
        res.status(200).json({ status: `SMS dispatch initiated to ${to}` });
    } catch (error) {
        console.error(""API Error sending SMS:"", error);
        res.status(500).json({ error: 'Failed to send SMS.' });
    }
});

/**
 * @api {post} /api/send/whatsapp Send a WhatsApp message (via Sandbox)
 * @apiName SendWhatsApp
 * @apiGroup Messages
 *
 * @apiBody {String} to Recipient WhatsApp number in E.164 format (must be allowlisted).
 * @apiBody {String} message Text content of the message.
 *
 * @apiSuccess {String} status Success message.
 * @apiError {String} error Error description.
 */
app.post('/api/send/whatsapp', async (req, res) => {
    const { to, message } = req.body;

    // Basic Input Validation
    if (!to || !message) {
        return res.status(400).json({ error: 'Missing ""to"" or ""message"" in request body.' });
    }
    // Basic format check (11-15 digits). For robust E.164 validation, use a library like libphonenumber-js.
     if (!/^\d{11,15}$/.test(to)) {
         return res.status(400).json({ error: 'Invalid ""to"" number format. The Vonage SDK expects E.164 format without the leading \'+\'.' });
    }

    // TODO: Add Authentication/Authorization middleware here for production
     console.log(`API request to send WhatsApp to ${to}`);

    try {
        await sendWhatsApp(to, message); // Using the Sandbox number by default
        res.status(200).json({ status: `WhatsApp dispatch initiated to ${to}` });
    } catch (error) {
        console.error(""API Error sending WhatsApp:"", error);
        res.status(500).json({ error: 'Failed to send WhatsApp message.' });
    }
});


// --- Server Start ---
const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`Server listening on port ${port}`);
    console.log(`ngrok forwarding URL (ensure it matches Vonage webhook config): Check your ngrok terminal.`);
    // You can add a startup check here to ensure essential env vars are present
    if (!process.env.VONAGE_SMS_FROM_NUMBER) console.warn('WARN: VONAGE_SMS_FROM_NUMBER not set. Cannot send SMS.');
    if (!process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER) console.warn('WARN: VONAGE_WHATSAPP_SANDBOX_NUMBER not set. Cannot send WhatsApp via Sandbox.');
    // Note: VONAGE_API_SIGNATURE_SECRET absence is now a fatal error at startup.
});

Explanation:

  1. Dependencies & Setup: We import necessary modules and initialize Express. dotenv.config() loads variables from .env.
  2. Vonage Client: We initialize the Vonage client using credentials from environment variables. Checks ensure essential credentials (including signature secret) exist, exiting if not. We comment out the apiHost override for the sandbox, preferring the default endpoint.
  3. sendSms Function: Takes toNumber and messageText, constructs the payload for the Messages API (channel: 'sms'), and uses vonage.messages.send(). Basic logging for success and failure is included.
  4. sendWhatsApp Function: Similar to sendSms, but uses the WhatsAppText class from @vonage/messages and specifies channel: 'whatsapp'. It uses the VONAGE_WHATSAPP_SANDBOX_NUMBER by default.
  5. rawBodyMiddleware & verifyWebhookSignature: This is crucial for security. Vonage signs webhook requests with a JWT using your Signature Secret. verifySignature needs the raw, unparsed request body. We create middleware (rawBodyMiddleware) using express.raw to capture this raw body before express.json() parses it for specific webhook routes. The verifyWebhookSignature function extracts the JWT from the Authorization header and uses @vonage/jwt's verifySignature method along with the raw body and your secret to validate the request. Crucially, if the signature secret is missing or verification fails, the function now returns false, preventing insecure processing.
  6. Webhook Endpoints (/webhooks/inbound, /webhooks/status):
    • These routes use the rawBodyMiddleware first, then express.json().
    • They must call verifyWebhookSignature to ensure the request is legitimate. Unauthorized requests are rejected with 401.
    • The /inbound handler logs the incoming message, extracts sender and content based on the channel, and optionally sends a reply using our helper functions.
    • The /status handler logs delivery status updates.
    • Both endpoints must respond with res.status(200).end() promptly to prevent Vonage from retrying the webhook.
  7. API Endpoints (/api/send/sms, /api/send/whatsapp):
    • Simple POST endpoints are added to trigger sending messages externally. These routes use the standard express.json() middleware applied after the webhook routes.
    • They perform basic validation on the request body (to, message), including a simple digit check for the phone number, with notes on using better validation libraries. The error message clarifies the SDK's expectation for E.164 format.
    • They call the respective sendSms or sendWhatsApp functions.
    • Note: These endpoints lack proper authentication/authorization, which is essential for production (see Section 3). The @api comments are noted as usable with apidoc.
  8. Server Start: The server starts listening on the configured PORT. We add checks/warnings on startup for missing optional environment variables.

How Do You Secure and Enhance Your Messaging API for Production?

The example above includes basic API endpoints (/api/send/sms, /api/send/whatsapp). For a production system, you would enhance this layer significantly:

  • Authentication & Authorization: Protect your API endpoints. Common methods include:

    • API Keys: Issue unique keys to clients. Clients send the key in a header (e.g., X-API-Key). Your server validates the key against a stored list.

    • JWT (JSON Web Tokens): Implement a login flow where users/systems get a JWT upon successful authentication. They include the JWT in the Authorization: Bearer <token> header for subsequent requests. Libraries like passport with passport-jwt or passport-http-bearer (for API keys) can help.

    • OAuth2: For third-party integrations or more complex authorization scenarios.

    • Implementation Example (Conceptual - using a simple API key check middleware):

      javascript
      const VALID_API_KEYS = ['your-super-secret-key-1', 'another-key']; // Store securely, e.g., in DB or vault
      
      function authenticateApiKey(req, res, next) {
          const apiKey = req.headers['x-api-key'];
          if (apiKey && VALID_API_KEYS.includes(apiKey)) {
              console.log('API Key authenticated.');
              next(); // Key is valid, proceed
          } else {
              console.warn('API Key authentication failed.');
              res.status(401).json({ error: 'Unauthorized: Invalid or missing API Key' });
          }
      }
      
      // Apply middleware to API routes
      app.post('/api/send/sms', authenticateApiKey, async (req, res) => { /* ... */ });
      app.post('/api/send/whatsapp', authenticateApiKey, async (req, res) => { /* ... */ });
  • Request Validation: Sanitize and validate all incoming data rigorously.

    • Use libraries like express-validator or joi to define schemas for request bodies and query parameters.

    • Check data types, lengths, formats (e.g., ensure phone numbers adhere to E.164 using libraries like libphonenumber-js).

    • Example using express-validator:

      bash
      npm install express-validator libphonenumber-js
      javascript
      const { body, validationResult } = require('express-validator');
      const { parsePhoneNumberFromString } = require('libphonenumber-js');
      
      app.post('/api/send/sms',
          authenticateApiKey, // Your auth middleware first
          // Validation rules
          body('to').custom(value => {
              // Note: Vonage SDK expects E.164 without '+', but validation should check standard format.
              // Prepend '+' for robust parsing if necessary, then remove it for the SDK call.
              const numberToValidate = value.startsWith('+') ? value : `+${value}`;
              const phoneNumber = parsePhoneNumberFromString(numberToValidate);
              if (!phoneNumber || !phoneNumber.isValid()) {
                  throw new Error('Invalid E.164 phone number format required (e.g., +14155550100)');
              }
              // Ensure the original format (without '+') is used later if needed by SDK
              // req.body.to = value; // Keep original format passed in if it was valid without '+'
              return true;
          }),
          body('message').notEmpty().isLength({ min: 1, max: 1600 }).withMessage('Message is required and must be between 1 and 1600 characters'),
          async (req, res) => {
              const errors = validationResult(req);
              if (!errors.isEmpty()) {
                  return res.status(400).json({ errors: errors.array() });
              }
              // Validation passed, proceed with req.body
              // Ensure you use the correct format of 'to' for the sendSms function
              const { to, message } = req.body;
              // ... call sendSms(to, message) ...
          }
      );

Frequently Asked Questions

Can I send WhatsApp messages outside the 24-hour window?

WhatsApp enforces a 24-hour customer service window. When a user messages you or calls you, a 24-hour timer starts. During this window, you can send any type of message for free. After 24 hours, you can only send pre-approved message templates. The window resets each time the user responds. Plan your messaging strategy accordingly – use templates to initiate conversations, then engage freely within the 24-hour window.

How do I verify that webhooks are coming from Vonage?

Vonage signs webhook requests with a JWT (JSON Web Token) using HMAC-SHA256 and your signature secret. Always verify the signature before processing webhook data. The signature secret should be at least 32 bits and stored securely. Compare the JWT from the Authorization header against your secret using the @vonage/jwt library's verifySignature method. Additionally, compare a SHA-256 hash of the payload to the payload_hash claim in the JWT to guard against token replay attacks. Reject any requests that fail verification with a 401 status code.

What's the difference between the Vonage Sandbox and production WhatsApp?

The Vonage WhatsApp Sandbox uses a shared test number (14157386102) and allows you to test WhatsApp integration without a full WhatsApp Business Account setup. You must "allowlist" your number by sending a specific message to the Sandbox number. For production, you need a registered WhatsApp Business number linked to your Vonage application. The Sandbox is ideal for development and testing, but production requires proper WhatsApp Business API approval and your own dedicated business phone number.

How do I handle message delivery failures?

Monitor the status webhook (/webhooks/status) for delivery updates. Messages can fail for various reasons: invalid phone numbers, undelivered due to network issues, or rejected by WhatsApp. Implement retry logic with exponential backoff for transient failures. Log all message UUIDs and their status updates to track delivery rates. For critical messages, consider implementing a fallback channel (e.g., SMS) when WhatsApp delivery fails. Always handle errors gracefully in your sendSms and sendWhatsApp functions.

What phone number format does Vonage expect?

Vonage expects phone numbers in E.164 format without the leading + symbol. E.164 format includes the country code followed by the subscriber number (e.g., 14155550100 for a US number). Use libraries like libphonenumber-js to validate and format phone numbers correctly before sending them to the Vonage API. Invalid phone number formats will result in API errors and failed message delivery.

How do I secure my API endpoints for production use?

Never deploy the example endpoints to production without authentication. Implement API key validation, JWT-based authentication, or OAuth2 depending on your use case. Store API keys in a secure database or secrets management service, never in code. Use HTTPS for all communications. Implement rate limiting to prevent abuse. Validate all input data rigorously using libraries like express-validator. Consider implementing IP whitelisting for additional security. Load your Vonage private key from environment variables or a secure vault, never commit it to version control.

Can I send media files via WhatsApp using Vonage?

Yes, the Vonage Messages API supports sending images, videos, audio files, and documents via WhatsApp. Use the appropriate message type classes from @vonage/messages (e.g., WhatsAppImage, WhatsAppVideo, WhatsAppAudio, WhatsAppFile). Media files must be publicly accessible via HTTPS URL, or you can upload them using Vonage's media API. WhatsApp has file size limits: images (5 MB), videos (16 MB), audio (16 MB), and documents (100 MB). The file format must be supported by WhatsApp.

How do I test webhooks locally?

Use ngrok to expose your local Express server to the internet. Run ngrok http 3000 to create a public HTTPS URL that forwards to your local port 3000. Copy the ngrok HTTPS URL and configure it in your Vonage Application settings for both Inbound URL and Status URL (e.g., https://abc123.ngrok-free.app/webhooks/inbound). Ngrok provides a web interface at http://localhost:4040 where you can inspect all webhook requests and responses, making debugging easier.

What are the rate limits for the Vonage Messages API?

Vonage implements rate limiting to ensure fair usage and system stability. The specific limits depend on your account type and the messaging channel. WhatsApp has its own rate limits based on your business tier (Tier 1: 1,000 unique recipients per 24 hours, scaling up with verified quality). Implement proper error handling to catch rate limit errors (HTTP 429) and implement exponential backoff retry logic. Consider implementing a message queue for high-volume applications to smooth out traffic spikes and stay within rate limits.

How do I migrate from the Sandbox to production WhatsApp?

To move to production WhatsApp, register for a WhatsApp Business Account through Vonage. Complete the business verification process with Meta (Facebook). Once approved, you'll receive a dedicated WhatsApp Business number. Update your .env file to use your production WhatsApp number instead of the Sandbox number. Link your production number to your Vonage Application. Update webhook URLs if needed. Test thoroughly in production with real customer opt-ins. Ensure compliance with WhatsApp's Business Policy and Commerce Policy.


Related Resources: