code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

Send and Receive WhatsApp Messages with Node.js and Vonage

A guide to building a Node.js/Express application using the Vonage Messages API for WhatsApp communication, covering setup, webhooks, security, and deployment.

This guide provides a complete walkthrough for building a Node.js and Express application capable of sending and receiving WhatsApp messages using the Vonage Messages API. We will cover everything from initial project setup and core messaging logic to security, error handling, and deployment considerations.

By the end of this tutorial, you will have a functional application that can:

  1. Receive incoming WhatsApp messages via a webhook.
  2. Verify the authenticity of incoming messages using JWT signatures.
  3. Send WhatsApp message replies back to the user.
  4. Handle message status updates via a separate webhook.

This enables developers to integrate WhatsApp communication directly into their applications for notifications, customer support, chatbots, and more.

Project Overview and Goals

What We're Building: A Node.js server using the Express framework that listens for incoming WhatsApp messages and message status updates sent from the Vonage platform via webhooks. When a message arrives, the server verifies it, logs it, and sends a confirmation reply back to the sender's WhatsApp number.

Problem Solved: This provides a foundational backend service for two-way WhatsApp communication, enabling applications to interact with users on a widely adopted messaging platform without needing to manage the underlying complexities of the WhatsApp Business API directly.

Technologies Used:

  • Node.js: A JavaScript runtime environment for building the server-side application. (Version 20 or higher recommended).
  • Express: A minimal and flexible Node.js web application framework used to handle HTTP requests and define webhook endpoints.
  • Vonage Messages API: A unified API from Vonage for sending and receiving messages across various channels, including WhatsApp, SMS, MMS, and more.
  • Vonage Node SDK (@vonage/server-sdk, @vonage/messages, @vonage/jwt): Simplifies interaction with the Vonage APIs, including sending messages and verifying signatures.
  • ngrok: A tool to expose your local development server to the internet, making it reachable by Vonage webhooks during testing.
  • dotenv: A module to load environment variables from a .env file into process.env.

System Architecture:

text
+-------------+       +-----------------+       +-----------------+       +-----------------+       +-------------------------+
| End User    |<----->| WhatsApp        |<----->| Vonage Platform |<----->| ngrok Tunnel    |<----->| Node.js/Express App     |
| (WhatsApp)  |       | Infrastructure  |       | (Messages API)  |       | (Public URL)    |       | (localhost:PORT)        |
+-------------+       +-----------------+       +-----------------+       +-----------------+       +-------------------------+
     |                                                 |  /inbound webhook                      |             | Process incoming msg
     |------------------ Sends Message ----------------->|---------------------------------------->             | Verify JWT
     |                                                 |                                        |             | Call Vonage API to send reply
     |<----------------- Receives Reply --------------- |<----------------------------------------             |
     |                                                 |                                        |             |
     |                                                 |  /status webhook                       |             | Log status update
     |                                                 |<----------------------------------------             | Verify JWT

Note: ASCII diagrams render best with fixed-width fonts. A graphical diagram might be preferable for final publication.

Prerequisites:

  • Node.js and npm: Ensure Node.js (v20+) and npm are installed. You can download them from nodejs.org.
  • Vonage API Account: Sign up for a free account at vonage.com. You'll get free credits to start.
  • ngrok: Install ngrok from ngrok.com and create a free account.
  • WhatsApp Account: A standard WhatsApp account on your smartphone for testing the Sandbox.

1. Setting up the Project

Let's initialize our Node.js project and install the necessary dependencies.

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

    bash
    mkdir vonage-whatsapp-app
    cd vonage-whatsapp-app
  2. Initialize npm: Initialize the project using npm, accepting the defaults. This creates a package.json file.

    bash
    npm init -y
  3. Install Dependencies: Install Express for the web server, the Vonage SDKs for interacting with the API, and dotenv for managing environment variables.

    bash
    npm install express dotenv @vonage/server-sdk @vonage/messages @vonage/jwt
    • express: Web framework for handling HTTP requests (webhooks).
    • dotenv: Loads environment variables from a .env file.
    • @vonage/server-sdk: Core Vonage SDK.
    • @vonage/messages: Specific helpers for the Messages API.
    • @vonage/jwt: Utilities for handling JWTs, specifically for signature verification.

    Note: npm install without flags like --save or --save-dev defaults to adding packages as production dependencies in recent npm versions. This also generates/updates package-lock.json, which is important for deterministic installs, especially when using npm ci later in CI/CD pipelines.

  4. Create Project Structure: Create a source directory and the main server file.

    bash
    mkdir src
    touch src/server.js
  5. Create .env File: This file will store sensitive credentials and configuration. Never commit this file to version control.

    bash
    touch .env

    We will populate this file later in the guide.

  6. Create .gitignore File: Prevent sensitive files and unnecessary directories from being committed to Git.

    bash
    touch .gitignore

    Add the following lines to your .gitignore file:

    text
    # Dependencies
    node_modules/
    
    # Environment variables
    .env
    
    # Private Key (if stored locally - see Security section)
    private.key
    
    # Logs
    logs
    *.log
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    lerna-debug.log*
    
    # Optional Editor directories/files
    .vscode/*
    !.vscode/settings.json
    !.vscode/tasks.json
    !.vscode/launch.json
    !.vscode/extensions.json
    *.sublime-workspace
    
    # Optional Operating System files
    .DS_Store
    Thumbs.db

Your basic project structure should now look like this:

vonage-whatsapp-app/ ├── node_modules/ ├── src/ │ └── server.js ├── .env ├── .gitignore ├── package.json └── package-lock.json

This setup provides a clean separation of source code, dependencies, and configuration.

2. Implementing Core Functionality

Now, let's write the core logic in src/server.js to handle incoming messages and send replies.

src/server.js

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

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

const app = express();
app.use(express.json()); // Middleware to parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Middleware to parse URL-encoded bodies

// --- Configuration ---
const PORT = process.env.PORT || 8000; // Use port from .env or default to 8000
const VONAGE_API_KEY = process.env.VONAGE_API_KEY;
const VONAGE_API_SECRET = process.env.VONAGE_API_SECRET;
const VONAGE_APPLICATION_ID = process.env.VONAGE_APPLICATION_ID;
const VONAGE_PRIVATE_KEY = process.env.VONAGE_PRIVATE_KEY; // Path to your private.key file
const VONAGE_WHATSAPP_NUMBER = process.env.VONAGE_WHATSAPP_NUMBER; // Vonage WhatsApp Sandbox number
const VONAGE_API_SIGNATURE_SECRET = process.env.VONAGE_API_SIGNATURE_SECRET;

// --- Input Validation (Basic Checks) ---
if (!VONAGE_API_KEY || !VONAGE_API_SECRET || !VONAGE_APPLICATION_ID || !VONAGE_PRIVATE_KEY || !VONAGE_WHATSAPP_NUMBER || !VONAGE_API_SIGNATURE_SECRET) {
    console.error('[ERROR] Missing required Vonage environment variables. Please check your .env file.');
    process.exit(1); // Exit if essential config is missing
}

// --- Initialize Vonage Client ---
const vonage = new Vonage(
    {
        apiKey: VONAGE_API_KEY,
        apiSecret: VONAGE_API_SECRET,
        applicationId: VONAGE_APPLICATION_ID,
        privateKey: VONAGE_PRIVATE_KEY,
    },
    {
        apiHost: 'https://messages-sandbox.nexmo.com' // Use Sandbox URL for testing
        // For production: Remove the 'apiHost' option.
        // The SDK defaults to the production Messages API endpoint (e.g., 'https://api.nexmo.com/v1/messages').
        // Explicitly setting it to 'https://api.nexmo.com' is also possible if needed.
    }
);

// --- JWT Signature Verification Middleware ---
// Verifies that incoming webhooks genuinely originated from Vonage
const verifyVonageSignature = (req, res, next) => {
    try {
        // Get JWT from Authorization header (format: Bearer <token>)
        const authHeader = req.headers.authorization;
        if (!authHeader || !authHeader.startsWith('Bearer ')) {
            console.warn('[WARN] Missing or invalid Authorization header.');
            return res.status(401).send('Unauthorized: Missing Bearer token.');
        }
        const jwtToken = authHeader.split(' ')[1];

        // Verify the signature using the secret from .env
        if (!verifySignature(jwtToken, VONAGE_API_SIGNATURE_SECRET)) {
            console.error('[ERROR] Invalid JWT signature.');
            return res.status(401).send('Unauthorized: Invalid signature.');
        }
        console.log('[INFO] JWT Signature Verified');
        next(); // Proceed to the next middleware/handler if signature is valid
    } catch (error) {
        console.error('[ERROR] Error verifying JWT:', error);
        res.status(500).send('Internal Server Error during signature verification.');
    }
};

// --- Function to Send WhatsApp Message ---
const sendMessage = async (to_number, text = 'Message received.') => {
    console.log(`[INFO] Attempting to send message to ${to_number}...`);
    try {
        const { message_uuid } = await vonage.messages.send(
            new WhatsAppText({
                from: VONAGE_WHATSAPP_NUMBER, // Must be the Vonage Sandbox number or your provisioned WA number
                to: to_number,               // The recipient's phone number
                text: text,                  // The message content
                client_ref: `msg-to-${to_number}-${Date.now()}` // Optional client reference
            })
        );
        console.log(`[SUCCESS] Message sent successfully to ${to_number}. Message UUID: ${message_uuid}`);
        return message_uuid; // Return the UUID for tracking
    } catch (error) {
        console.error(`[ERROR] Error sending message to ${to_number}:`, error.response ? error.response.data : error.message);
        // Log detailed Vonage API error if available
        if (error.response && error.response.data) {
            console.error('[ERROR] Vonage API Error Details:', JSON.stringify(error.response.data, null, 2));
        }
        // Re-throw the error or handle it as needed
        throw error;
    }
};

// --- Webhook Endpoints ---

// 1. Inbound Message Webhook (/inbound)
// Handles messages sent *from* WhatsApp users *to* your Vonage number
app.post('/inbound', verifyVonageSignature, async (req, res) => {
    console.log('[INFO] --- Received Inbound Message ---');
    console.log('[INFO] Request Body:', JSON.stringify(req.body, null, 2));

    const { from: requesterNumber, message } = req.body;

    // Basic validation of incoming payload
    if (!requesterNumber || !message || !message.content || message.content.type !== 'text') {
        console.warn('[WARN] Received incomplete or non-text message payload.');
        // Acknowledge receipt even if we don't process it fully here
        return res.status(200).send('Payload received but not processed (expected text message).');
    }

    const incomingText = message.content.text;
    console.log(`[INFO] Received text message ""${incomingText}"" from ${requesterNumber}`);

    try {
        // Send a reply message back to the user
        await sendMessage(requesterNumber, `[REPLY] Got it! You said: ""${incomingText}""`);
        res.status(200).send('Message processed and reply sent.'); // Acknowledge successful processing
    } catch (error) {
        console.error('[ERROR] Error handling incoming message or sending reply:', error);
        // Send 500 but Vonage might retry. Log carefully.
        res.status(500).send('An error occurred while processing the message.');
    }
});

// 2. Status Update Webhook (/status)
// Handles delivery receipts and status updates for messages *sent from* your application
app.post('/status', verifyVonageSignature, (req, res) => {
    console.log('[INFO] --- Received Status Update ---');
    console.log('[INFO] Request Body:', JSON.stringify(req.body, null, 2));

    // Process the status update (e.g., update message status in a database)
    const { message_uuid, status, timestamp, to, error } = req.body;
    console.log(`[INFO] Status update for message ${message_uuid} to ${to}: ${status} at ${timestamp}`);
    if (error) {
        console.error(`[ERROR] Message delivery error: ${error.type} - ${error.reason}`);
    }

    // Acknowledge receipt to Vonage
    res.status(200).send('Status received.');
});

// 3. Health Check Endpoint (/health)
// Simple endpoint for monitoring services to check if the app is running
app.get('/health', (req, res) => {
    res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});


// --- Start the Server ---
const server = app.listen(PORT, () => { // Assign server to variable for graceful shutdown
    console.log(`[INFO] Server is running and listening on http://localhost:${PORT}`);
    console.log(`[INFO] Webhook endpoints:`);
    console.log(`  POST /inbound (for incoming messages)`);
    console.log(`  POST /status (for message status updates)`);
    console.log(`[INFO] Ensure ngrok exposes port ${PORT} and webhooks are configured in Vonage.`);
});

// --- Graceful Shutdown Handling (Optional but Recommended) ---
const cleanup = (signal) => {
    console.info(`[INFO] ${signal} signal received: closing HTTP server`);
    server.close(() => {
        console.log('[INFO] HTTP server closed.');
        // Add other cleanup logic here (e.g., close database connections)
        process.exit(0);
    });
};

process.on('SIGTERM', () => cleanup('SIGTERM'));
process.on('SIGINT', () => cleanup('SIGINT'));

// Export app for testing purposes
module.exports = { app, sendMessage }; // Export app and potentially other functions if needed for testing

Explanation:

  1. Dependencies & Config: Imports necessary modules, loads .env variables, and sets up basic configuration constants. Includes checks for essential variables.
  2. Vonage Client: Initializes the Vonage client with credentials from .env. The apiHost option directs requests to the Sandbox environment. Removed or changed for production use.
  3. verifyVonageSignature Middleware: This crucial function intercepts requests to webhook endpoints. It extracts the JWT from the Authorization: Bearer <token> header and uses verifySignature from @vonage/jwt along with your VONAGE_API_SIGNATURE_SECRET to confirm the request genuinely came from Vonage. Unauthorized requests are rejected with a 401 status.
  4. sendMessage Function: Encapsulates the logic for sending a WhatsApp message using vonage.messages.send. It constructs a WhatsAppText object and handles potential errors, logging success or failure details.
  5. /inbound Webhook:
    • Listens for POST requests on /inbound.
    • Runs the verifyVonageSignature middleware first.
    • Parses the request body (req.body) to get the sender's number (from) and message content.
    • Logs the incoming message details.
    • Calls sendMessage to send a reply.
    • Responds with HTTP 200 OK to Vonage to acknowledge receipt and prevent retries. Error handling returns 500.
  6. /status Webhook:
    • Listens for POST requests on /status.
    • Runs the verifyVonageSignature middleware.
    • Logs the received status update details (e.g., delivered, read, failed).
    • Responds with 200 OK. In a real application, you'd update your database here.
  7. /health Endpoint: A simple GET endpoint useful for monitoring tools to check if the server is alive.
  8. Server Start: Starts the Express server, listening on the configured PORT.
  9. Graceful Shutdown: Basic signal handling for SIGTERM and SIGINT (common signals for stopping processes) ensures cleaner shutdowns by closing the server before exiting.
  10. Exports: The app instance and sendMessage function are exported at the bottom. This is a common pattern to allow importing them into test files.

3. API Layer Details (Webhooks)

In this application, the ""API layer"" consists of the webhook endpoints that Vonage interacts with. There isn't a separate user-facing REST API being built here.

Authentication & Authorization: Handled exclusively by the verifyVonageSignature middleware, ensuring only legitimate requests from Vonage are processed.

Request Validation: Basic validation is included in the /inbound handler to check for necessary fields (from, message.content.type === 'text'). For production, use libraries like express-validator for more robust schema validation.

Endpoints:

  1. POST /inbound

    • Purpose: Receives messages sent from users to your Vonage WhatsApp number.
    • Authentication: Authorization: Bearer <JWT_TOKEN> (Verified by middleware).
    • Request Body (Example JSON):
      json
      {
        ""message_uuid"": ""a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8"",
        ""to"": ""14157386102"",
        ""from"": ""1XXXXXXXXXX"",
        ""timestamp"": ""2023-10-27T10:30:00.123Z"",
        ""channel"": ""whatsapp"",
        ""message"": {
          ""content"": {
            ""type"": ""text"",
            ""text"": ""Hello Vonage!""
          }
        },
        ""context"": {
           ""message_uuid"": ""..."",
           ""message_from"": ""...""
        },
        ""profile"": {
          ""name"": ""Jane Doe""
        }
      }
    • Response:
      • 200 OK (Plain text or JSON): Indicates successful receipt and processing.
      • 401 Unauthorized: If JWT signature verification fails.
      • 500 Internal Server Error: If an error occurs during processing.
  2. POST /status

    • Purpose: Receives status updates for messages you sent using the API.
    • Authentication: Authorization: Bearer <JWT_TOKEN> (Verified by middleware).
    • Request Body (Example JSON - Delivered):
      json
      {
        ""message_uuid"": ""e295613c-a2fc-40f4-a9ae-fa16f5c685cf"",
        ""to"": ""1XXXXXXXXXX"",
        ""from"": ""14157386102"",
        ""timestamp"": ""2023-10-27T10:30:05.456Z"",
        ""status"": ""delivered"",
        ""channel"": ""whatsapp"",
        ""usage"": {
          ""currency"": ""EUR"",
          ""price"": ""0.0075""
        },
        ""client_ref"": ""msg-to-1XXXXXXXXXX-1678886400000""
      }
    • Response:
      • 200 OK: Acknowledges receipt of the status.
      • 401 Unauthorized: If JWT signature verification fails.

Testing with curl (Simulating Vonage): Directly testing webhooks that require valid, signed JWTs with curl is difficult. You would need to generate a valid JWT signed with the VONAGE_API_SIGNATURE_SECRET. For local testing, triggering the flow by sending a real WhatsApp message (see Section 5) is the most practical approach. Automated integration tests (also Section 5) can mock the signature verification.

4. Integrating with Vonage (Setup & Configuration)

This section details the necessary steps within the Vonage Dashboard and your local environment.

  1. Sign Up/In to Vonage: If you haven't already, create an account at vonage.com.

  2. Create a Vonage Application:

    • Navigate to Applications in the Vonage API Dashboard.
    • Click Create a new application.
    • Give it a name (e.g., ""Node WhatsApp Demo"").
    • Under Capabilities, toggle Messages ON.
    • You'll see fields for Inbound URL and Status URL. We'll fill these after setting up ngrok.
    • Click Generate public and private key. A private.key file will download.
    • Security Warning: The guide uses ./private.key in .env for simplicity, assuming you save the key in your project root. This is NOT recommended for production. Storing private keys directly in your application directory increases risk if your code repository or server filesystem is compromised.
    • Recommendation: For production, store the private.key file outside your project directory (e.g., in a secure parent directory inaccessible to the web server) and use an absolute path or path relative to the user's home directory in .env. Alternatively, use a dedicated secrets management system (like HashiCorp Vault, AWS Secrets Manager, Google Secret Manager) to inject the key content as an environment variable or mount it securely. Ensure the key file has strict permissions (read-only for the application user). Remember to add private.key to your .gitignore if storing it locally during development.
    • Click Generate new application.
    • Note down the Application ID displayed.
  3. Start ngrok: Open a new terminal window (keep your Node server terminal running later). Run ngrok to expose the port your Express server will listen on (default is 8000, or whatever you set in .env).

    bash
    # Make sure you are logged into ngrok (ngrok config add-authtoken YOUR_TOKEN)
    ngrok http 8000

    ngrok will display output similar to this:

    text
    Session Status                online
    Account                       Your Name (Plan: Free)
    Version                       x.x.x
    Region                        United States (us)
    Forwarding                    https://<UNIQUE_SUBDOMAIN>.ngrok-free.app -> http://localhost:8000

    Copy the https://<UNIQUE_SUBDOMAIN>.ngrok-free.app URL. This is your public base URL. Keep this ngrok terminal running.

  4. Configure Webhooks in Vonage Application:

    • Go back to the Vonage Application you created.
    • In the Messages capability section:
      • Set Inbound URL to: YOUR_NGROK_URL/inbound (e.g., https://<UNIQUE_SUBDOMAIN>.ngrok-free.app/inbound)
      • Set Status URL to: YOUR_NGROK_URL/status (e.g., https://<UNIQUE_SUBDOMAIN>.ngrok-free.app/status)
    • Scroll down and click Save changes.
  5. Set up Messages API Sandbox for WhatsApp:

    • In the Vonage Dashboard sidebar, go to Developer Tools -> Messages API Sandbox.
    • Click the WhatsApp tab.
    • Scan the QR code with your phone's WhatsApp application or send the specified phrase (e.g., ""JOIN <keyword>"") to the provided Vonage Sandbox phone number. This allowlists your WhatsApp number to interact with the Sandbox.
    • Configure Webhooks for the Sandbox: Click the ""Webhooks"" tab within the Sandbox page.
      • Set Inbound URL to: YOUR_NGROK_URL/inbound
      • Set Status URL to: YOUR_NGROK_URL/status
      • Click Save webhooks. This ensures messages to the Sandbox number and status updates from it are routed to your local server via ngrok.
  6. Populate the .env File: Open the .env file in your project and add the following variables, replacing the placeholder values with your actual credentials:

    dotenv
    # .env
    
    # Vonage API Credentials (Found on Vonage Dashboard homepage)
    VONAGE_API_KEY=""YOUR_API_KEY""
    VONAGE_API_SECRET=""YOUR_API_SECRET""
    
    # Vonage Application ID (From the application you created)
    VONAGE_APPLICATION_ID=""YOUR_APPLICATION_ID""
    
    # Path to your downloaded private key file
    # IMPORTANT: See security warning in Section 4. For development, if the key is
    # in the project root, this path is correct. For production, use a secure location.
    VONAGE_PRIVATE_KEY=""./private.key""
    
    # Vonage WhatsApp Sandbox Number (From Messages API Sandbox page)
    # Use the format without '+' or leading '00', e.g., 14157386102
    VONAGE_WHATSAPP_NUMBER=""14157386102""
    
    # Vonage Signature Secret (Found in Vonage Dashboard -> Settings -> API settings)
    # Used to verify webhook signatures. Use the 'Primary' or 'Secondary' secret.
    VONAGE_API_SIGNATURE_SECRET=""YOUR_SIGNATURE_SECRET""
    
    # Server Port (Optional, defaults to 8000 in server.js if not set)
    PORT=8000
    • VONAGE_API_KEY / VONAGE_API_SECRET: Found on the main page of your Vonage API Dashboard.
    • VONAGE_APPLICATION_ID: Found on the specific application page you created in step 2.
    • VONAGE_PRIVATE_KEY: The path to the private.key file you downloaded. Adjust if you saved it elsewhere, following security best practices.
    • VONAGE_WHATSAPP_NUMBER: The dedicated phone number shown on the Messages API Sandbox page for WhatsApp.
    • VONAGE_API_SIGNATURE_SECRET: Go to Settings in the Vonage Dashboard. Under API settings, find your Signature secret. Use either the primary or secondary secret here. This is crucial for webhook security.

5. Error Handling, Logging, and Retry Mechanisms

  • Error Handling Strategy:
    • Use try...catch blocks around asynchronous operations, especially Vonage API calls (sendMessage) and within webhook handlers (/inbound).
    • Log errors using console.error with detailed messages, including stack traces where relevant. Use structured prefixes like [ERROR].
    • Distinguish between expected errors (e.g., invalid input from user) and unexpected server/API errors.
    • Respond to Vonage webhooks appropriately: 200 OK for success, 4xx for client errors (like bad signature), 5xx for server errors. Returning non-200 for inbound messages might cause Vonage to retry.
  • Logging:
    • Current implementation uses console.log, console.warn, console.error with text prefixes. This is suitable for development.
    • Production Logging: Use a dedicated logging library like Pino or Winston.
      • Configure structured logging (JSON format) for easier parsing by log aggregation tools (e.g., Datadog, Splunk, ELK stack).
      • Set appropriate log levels (e.g., INFO, WARN, ERROR).
      • Include context in logs (e.g., message UUID, user number, request ID).
      • Example with Pino (conceptual):
        javascript
        // const pino = require('pino')();
        // pino.info({ msgUuid: message_uuid, status: status }, 'Received status update');
        // pino.error({ err: error, msgUuid: message_uuid }, 'Error sending message');
  • Retry Mechanisms:
    • Vonage Webhook Retries: Vonage automatically retries sending webhooks (inbound/status) if your server doesn't respond with 200 OK within a timeout period. It uses an exponential backoff strategy. Ensure your endpoints respond quickly or handle potential duplicate deliveries (idempotency).
    • Outbound Message Retries: The current sendMessage function doesn't implement retries. For critical messages, you could wrap the vonage.messages.send call in a retry loop (e.g., using libraries like async-retry) with exponential backoff for transient network or API errors (e.g., 5xx responses from Vonage). Be cautious not to retry indefinitely or for non-recoverable errors (e.g., 4xx responses like invalid number).

6. Database Schema and Data Layer

This simple example application is stateless—it receives a message and sends an immediate reply without storing conversation history.

For Stateful Applications:

If you need to track conversations, user state, message history, or integrate with other user data, you'll need a database.

  • Database Choice: PostgreSQL, MongoDB, MySQL, etc.
  • Schema Considerations (Example - PostgreSQL):
    sql
    CREATE TABLE users (
        phone_number VARCHAR(20) PRIMARY KEY, -- User's WhatsApp number
        whatsapp_name VARCHAR(255),
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
        last_seen_at TIMESTAMP WITH TIME ZONE
    );
    
    CREATE TABLE messages (
        message_uuid UUID PRIMARY KEY,       -- Internal message ID (optional, can use vonage_message_uuid)
        vonage_message_uuid VARCHAR(50) UNIQUE NOT NULL, -- Vonage's UUID for linking inbound/status
        user_phone_number VARCHAR(20) REFERENCES users(phone_number),
        direction VARCHAR(10) NOT NULL,      -- 'inbound' or 'outbound'
        channel VARCHAR(20) DEFAULT 'whatsapp',
        message_type VARCHAR(20) NOT NULL,   -- 'text', 'image', 'audio', etc.
        content TEXT,                       -- Text content or reference to media
        media_url VARCHAR(1024),            -- URL for images, audio, etc.
        status VARCHAR(20),                  -- 'submitted', 'delivered', 'read', 'failed'
        vonage_status_timestamp TIMESTAMP WITH TIME ZONE,
        error_code VARCHAR(50),
        error_reason TEXT,
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
        client_ref VARCHAR(255)             -- Optional client reference
    );
    
    CREATE INDEX idx_messages_user_phone ON messages(user_phone_number);
    CREATE INDEX idx_messages_status ON messages(status);
    CREATE INDEX idx_messages_created_at ON messages(created_at);
  • Data Access Layer: Use an ORM (Object-Relational Mapper) like Prisma or Sequelize (for SQL databases) or an ODM (Object-Document Mapper) like Mongoose (for MongoDB) to interact with the database from your Node.js code. This simplifies queries, migrations, and data modeling.
  • Migrations: Use the migration tools provided by your ORM (e.g., prisma migrate dev, sequelize db:migrate) to manage schema changes over time.
  • Implementation:
    • In /inbound, you would query the users table (create if not exists) and insert a new record into messages with direction='inbound' and the vonage_message_uuid from the request.
    • Before calling sendMessage, you would insert a record into messages with direction='outbound', status='submitted', and the vonage_message_uuid returned by the API call.
    • In /status, you would query the messages table using message_uuid (the Vonage UUID) and update the status, vonage_status_timestamp, and error fields based on the webhook payload.

7. Security Features

  • Webhook Signature Verification (CRITICAL): Already implemented via verifyVonageSignature. This prevents attackers from sending fake webhook requests to your endpoints. Ensure VONAGE_API_SIGNATURE_SECRET is kept confidential and managed securely.
  • Private Key Security (CRITICAL): As mentioned in Section 4, do not store your private.key file within your web root or commit it to version control. Use secure storage outside the project directory or a secrets management system.
  • Input Validation and Sanitization:
    • Validate incoming webhook payloads against expected schemas

Frequently Asked Questions

How to send WhatsApp messages with Node.js?

Use the Vonage Messages API with the Node.js SDK. The `sendMessage` function in the provided example demonstrates how to use `vonage.messages.send()` with a `WhatsAppText` object to send messages. This handles constructing the message payload and sending it via the Vonage platform.

What is the Vonage Messages API?

The Vonage Messages API is a unified API that allows developers to send and receive messages across various channels, including WhatsApp, SMS, and MMS. It simplifies the process of integrating messaging into applications and handles the underlying complexities of each channel.

Why does Vonage use JWT for webhook security?

Vonage uses JSON Web Tokens (JWT) for webhook security to ensure that incoming webhook requests genuinely originate from Vonage. This prevents malicious actors from spoofing webhook requests and triggering unintended actions on your server.

When should I use the Vonage Messages API Sandbox?

Use the Vonage Messages API Sandbox during development and testing to avoid incurring charges while experimenting with the API. This allows you to test your integration thoroughly before deploying it to production.

Can I store private keys in my project directory?

Storing private keys directly in your project directory is highly discouraged for security reasons. If compromised, this would allow access to credentials. Use secure external storage or a dedicated secrets management system instead.

How to receive WhatsApp messages in Node.js?

Set up a webhook endpoint (e.g., `/inbound`) on your Node.js server that the Vonage Messages API will send incoming WhatsApp messages to. Ensure the endpoint verifies the JWT signature to validate message authenticity.

What is the Vonage Node SDK?

The Vonage Node.js SDK (`@vonage/server-sdk`, `@vonage/messages`, `@vonage/jwt`) simplifies interaction with the Vonage APIs. This includes functionality for sending messages, verifying signatures, and managing other aspects of the communication flow.

Why use ngrok with Vonage webhooks?

ngrok creates a public tunnel to your locally running server, making it accessible from the internet. This allows Vonage webhooks to reach your local development environment during testing.

When to use a database with the Vonage Messages API?

If your application needs to store conversation history, user data, or manage message status beyond immediate replies, you'll need a database. The article provides an example database schema and implementation guidance.

How to verify Vonage webhook signatures?

Use the `@vonage/jwt` library's `verifySignature` function along with your `VONAGE_API_SIGNATURE_SECRET` from the Vonage dashboard to verify the JWT signature of webhook requests. The provided example code includes a middleware function to handle this.

What are Vonage webhook retries?

Vonage automatically retries webhook deliveries if your server doesn't respond with a 200 OK. Be mindful of this when implementing error handling and consider idempotent designs for webhook endpoints.

How to handle Vonage message status updates?

Implement a webhook endpoint (e.g., `/status`) in your Node.js application to receive message status updates. This endpoint receives delivery, read, or failed status updates, allowing you to track message delivery in your system.

What is the purpose of a health check endpoint?

A health check endpoint (e.g., `/health`) provides a simple way to check the status of your application. Monitoring systems and load balancers can use it to ensure that your server is running and accessible.

How to handle errors with the Vonage Node.js SDK?

Use try-catch blocks around Vonage API calls (e.g., sendMessage) and within webhook handlers to catch potential errors. Log errors with details and consider implementing retry logic for transient errors (e.g., network issues).